playbook_ui 18.1.0 → 18.2.0.pre.alpha.PLAY3330requiredtypeaheadvalidationbug20501

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 (259) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +1 -4
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +9 -5
  4. data/app/pb_kits/playbook/pb_advanced_table/Components/TablePagination.tsx +28 -6
  5. data/app/pb_kits/playbook/pb_advanced_table/Components/VirtualizedTableView.tsx +6 -1
  6. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +26 -9
  7. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +2 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +137 -1
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_section_separator.jsx +90 -0
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_section_separator.md +5 -0
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_with_props.md +2 -1
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +1 -0
  13. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +1 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/table_body.rb +4 -4
  15. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +8 -3
  16. data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +19 -6
  17. data/app/pb_kits/playbook/pb_advanced_table/table_row.rb +1 -1
  18. data/app/pb_kits/playbook/pb_background/_background.tsx +3 -1
  19. data/app/pb_kits/playbook/pb_background/kit.schema.json +1 -0
  20. data/app/pb_kits/playbook/pb_caption/_caption.tsx +3 -1
  21. data/app/pb_kits/playbook/pb_caption/kit.schema.json +1 -0
  22. data/app/pb_kits/playbook/pb_container/_container.tsx +3 -1
  23. data/app/pb_kits/playbook/pb_container/kit.schema.json +1 -0
  24. data/app/pb_kits/playbook/pb_currency/_currency.tsx +3 -1
  25. data/app/pb_kits/playbook/pb_currency/kit.schema.json +1 -0
  26. data/app/pb_kits/playbook/pb_dashboard_value/_dashboard_value.tsx +3 -1
  27. data/app/pb_kits/playbook/pb_dashboard_value/kit.schema.json +1 -0
  28. data/app/pb_kits/playbook/pb_date_picker/_date_picker.scss +4 -0
  29. data/app/pb_kits/playbook/pb_date_picker/_date_picker.tsx +51 -49
  30. data/app/pb_kits/playbook/pb_date_picker/date_picker.html.erb +56 -56
  31. data/app/pb_kits/playbook/pb_date_picker/sass_partials/_calendar_input_icon.scss +1 -1
  32. data/app/pb_kits/playbook/pb_detail/_detail.tsx +3 -1
  33. data/app/pb_kits/playbook/pb_detail/kit.schema.json +1 -0
  34. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +16 -0
  35. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +190 -29
  36. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_custom_content.html.erb +58 -0
  37. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_custom_content.md +10 -0
  38. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_initial_options.jsx +45 -0
  39. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_initial_options.md +9 -0
  40. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_loading.jsx +43 -0
  41. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_loading.md +9 -0
  42. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_multi_select.jsx +44 -0
  43. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_multi_select.md +5 -0
  44. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_multi_select_rails.html.erb +44 -0
  45. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_multi_select_rails.md +5 -0
  46. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_rich_results.jsx +64 -0
  47. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_rich_results.md +5 -0
  48. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_search.html.erb +43 -0
  49. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_search.md +55 -0
  50. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_controlled_selection.jsx +42 -0
  51. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_controlled_selection.md +5 -0
  52. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_external_search.jsx +58 -0
  53. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_external_search.md +9 -0
  54. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_no_options_text.html.erb +9 -0
  55. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_no_options_text.jsx +35 -0
  56. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_no_options_text_rails.md +3 -0
  57. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_no_options_text_react.md +3 -0
  58. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom.md +7 -2
  59. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom_rails.md +7 -2
  60. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.html.erb +33 -0
  61. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.jsx +42 -0
  62. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.md +24 -0
  63. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_highlight.jsx +101 -0
  64. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_highlight.md +3 -0
  65. data/app/pb_kits/playbook/pb_dropdown/docs/_playground.json +174 -3
  66. data/app/pb_kits/playbook/pb_dropdown/docs/_playground.overrides.json +154 -2
  67. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +14 -0
  68. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +9 -0
  69. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +4 -1
  70. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +14 -0
  71. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +1344 -0
  72. data/app/pb_kits/playbook/pb_dropdown/dropdown_container.html.erb +1 -1
  73. data/app/pb_kits/playbook/pb_dropdown/dropdown_container.rb +1 -0
  74. data/app/pb_kits/playbook/pb_dropdown/dropdown_index.test.js +1153 -0
  75. data/app/pb_kits/playbook/pb_dropdown/hooks/useAsyncOptions.ts +179 -0
  76. data/app/pb_kits/playbook/pb_dropdown/hooks/useHandleOnKeydown.tsx +21 -19
  77. data/app/pb_kits/playbook/pb_dropdown/index.js +518 -79
  78. data/app/pb_kits/playbook/pb_dropdown/keyboard_accessibility.js +5 -3
  79. data/app/pb_kits/playbook/pb_dropdown/kit.schema.json +78 -1
  80. data/app/pb_kits/playbook/pb_dropdown/quickpick/index.ts +60 -18
  81. data/app/pb_kits/playbook/pb_dropdown/quickpick_helper.rb +92 -3
  82. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownContainer.tsx +17 -3
  83. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownOption.tsx +21 -7
  84. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +5 -2
  85. data/app/pb_kits/playbook/pb_dropdown/subcomponents/MultiSelectTriggerDisplay.tsx +9 -3
  86. data/app/pb_kits/playbook/pb_dropdown/utilities/index.ts +8 -1
  87. data/app/pb_kits/playbook/pb_dropdown/utilities/quickpickDateSync.ts +137 -0
  88. data/app/pb_kits/playbook/pb_filter/Filter/InteractiveFilter.tsx +1 -1
  89. data/app/pb_kits/playbook/pb_form/docs/_description_react.md +3 -0
  90. data/app/pb_kits/playbook/pb_form/docs/_form_default_values.jsx +898 -0
  91. data/app/pb_kits/playbook/pb_form/docs/_form_default_values_react.md +3 -0
  92. data/app/pb_kits/playbook/pb_form/docs/_form_defaults.jsx +879 -0
  93. data/app/pb_kits/playbook/pb_form/docs/_form_defaults_react.md +29 -0
  94. data/app/pb_kits/playbook/pb_form/docs/_form_loading.jsx +75 -0
  95. data/app/pb_kits/playbook/pb_form/docs/{_form_loading.md → _form_loading_rails.md} +1 -0
  96. data/app/pb_kits/playbook/pb_form/docs/_form_loading_react.md +1 -0
  97. data/app/pb_kits/playbook/pb_form/docs/_form_model_values.jsx +147 -0
  98. data/app/pb_kits/playbook/pb_form/docs/_form_model_values_react.md +16 -0
  99. data/app/pb_kits/playbook/pb_form/docs/_form_multi_column.jsx +164 -0
  100. data/app/pb_kits/playbook/pb_form/docs/_form_multi_column_react.md +3 -0
  101. data/app/pb_kits/playbook/pb_form/docs/_form_validation.html.erb +27 -4
  102. data/app/pb_kits/playbook/pb_form/docs/_form_validation.jsx +1057 -0
  103. data/app/pb_kits/playbook/pb_form/docs/{_form_validation.md → _form_validation_rails.md} +4 -4
  104. data/app/pb_kits/playbook/pb_form/docs/_form_validation_react.md +26 -0
  105. data/app/pb_kits/playbook/pb_form/docs/example.yml +7 -0
  106. data/app/pb_kits/playbook/pb_form/docs/snippets/validation_checkbox.txt +6 -1
  107. data/app/pb_kits/playbook/pb_form/docs/snippets/validation_dropdown.txt +6 -1
  108. data/app/pb_kits/playbook/pb_form/docs/snippets/validation_multi_level_select.txt +1 -0
  109. data/app/pb_kits/playbook/pb_form/docs/snippets/validation_multi_select.txt +1 -0
  110. data/app/pb_kits/playbook/pb_form/docs/snippets/validation_phone_number.txt +1 -0
  111. data/app/pb_kits/playbook/pb_form/docs/snippets/validation_star_rating.txt +6 -1
  112. data/app/pb_kits/playbook/pb_form/docs/snippets/validation_textarea.txt +5 -1
  113. data/app/pb_kits/playbook/pb_form/docs/snippets/validation_time_zone_select.txt +1 -0
  114. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +52 -34
  115. data/app/pb_kits/playbook/pb_full_screen/_full_screen.scss +6 -1
  116. data/app/pb_kits/playbook/pb_full_screen/full_screen.test.jsx +19 -7
  117. data/app/pb_kits/playbook/pb_highlight/_highlight.tsx +3 -1
  118. data/app/pb_kits/playbook/pb_highlight/kit.schema.json +1 -0
  119. data/app/pb_kits/playbook/pb_icon_stat_value/_icon_stat_value.tsx +3 -1
  120. data/app/pb_kits/playbook/pb_icon_stat_value/kit.schema.json +1 -0
  121. data/app/pb_kits/playbook/pb_icon_value/_icon_value.tsx +3 -1
  122. data/app/pb_kits/playbook/pb_icon_value/kit.schema.json +1 -0
  123. data/app/pb_kits/playbook/pb_label_value/_label_value.tsx +3 -1
  124. data/app/pb_kits/playbook/pb_label_value/kit.schema.json +1 -0
  125. data/app/pb_kits/playbook/pb_legend/_legend.tsx +3 -1
  126. data/app/pb_kits/playbook/pb_legend/kit.schema.json +1 -0
  127. data/app/pb_kits/playbook/pb_lightbox/Header/_lightbox_header.tsx +1 -1
  128. data/app/pb_kits/playbook/pb_lightbox/lightbox.test.jsx +1 -1
  129. data/app/pb_kits/playbook/pb_map/docs/_map_default.jsx +1 -1
  130. data/app/pb_kits/playbook/pb_map/docs/_map_default.md +3 -3
  131. data/app/pb_kits/playbook/pb_map/docs/_map_with_custom_button.jsx +1 -1
  132. data/app/pb_kits/playbook/pb_map/docs/_map_with_plugin.jsx +1 -1
  133. data/app/pb_kits/playbook/pb_map/docs/_playground.json +3 -3
  134. data/app/pb_kits/playbook/pb_map/docs/_playground.overrides.json +3 -3
  135. data/app/pb_kits/playbook/pb_message/_message.tsx +3 -1
  136. data/app/pb_kits/playbook/pb_message/kit.schema.json +1 -0
  137. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +1 -0
  138. data/app/pb_kits/playbook/pb_multi_level_select/index.js +19 -4
  139. data/app/pb_kits/playbook/pb_multi_level_select/kit.schema.json +7 -0
  140. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.html.erb +4 -1
  141. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.rb +7 -3
  142. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +3 -1
  143. data/app/pb_kits/playbook/pb_multiple_users/kit.schema.json +1 -0
  144. data/app/pb_kits/playbook/pb_multiple_users_stacked/_multiple_users_stacked.tsx +4 -2
  145. data/app/pb_kits/playbook/pb_multiple_users_stacked/kit.schema.json +1 -0
  146. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +10 -7
  147. data/app/pb_kits/playbook/pb_phone_number_input/docs/_playground.json +1 -0
  148. data/app/pb_kits/playbook/pb_phone_number_input/index.ts +7 -2
  149. data/app/pb_kits/playbook/pb_phone_number_input/kit.schema.json +8 -0
  150. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.rb +5 -1
  151. data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.test.js +12 -0
  152. data/app/pb_kits/playbook/pb_progress_step/_progress_step_item.tsx +3 -1
  153. data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/MoreExtensionsDropdown.tsx +10 -3
  154. data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/Toolbar.tsx +16 -1
  155. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.json +2 -3
  156. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.overrides.json +2 -3
  157. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_attributes.jsx +1 -5
  158. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_default.jsx +1 -5
  159. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_default.md +2 -3
  160. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_focus.jsx +1 -5
  161. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_height.jsx +3 -13
  162. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_inline.jsx +1 -5
  163. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.jsx +2 -3
  164. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.jsx +1 -2
  165. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_min_height.jsx +3 -13
  166. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_preview.jsx +1 -5
  167. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.jsx +1 -5
  168. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_simple.jsx +1 -5
  169. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_sticky.jsx +1 -5
  170. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_templates.jsx +1 -5
  171. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_more_extensions.jsx +3 -8
  172. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_more_extensions.md +1 -1
  173. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_default.md +2 -2
  174. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.test.js +2 -1
  175. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_advanced.test.js +7 -5
  176. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_rails.js +15 -4
  177. data/app/pb_kits/playbook/pb_select/_select.scss +2 -2
  178. data/app/pb_kits/playbook/pb_star_rating/index.js +8 -4
  179. data/app/pb_kits/playbook/pb_star_rating/kit.schema.json +7 -0
  180. data/app/pb_kits/playbook/pb_star_rating/star_rating.rb +6 -1
  181. data/app/pb_kits/playbook/pb_stat_value/_stat_value.tsx +3 -1
  182. data/app/pb_kits/playbook/pb_stat_value/kit.schema.json +1 -0
  183. data/app/pb_kits/playbook/pb_table/_table.tsx +70 -197
  184. data/app/pb_kits/playbook/pb_table/index.ts +53 -122
  185. data/app/pb_kits/playbook/pb_table/styles/_sticky_columns.scss +26 -3
  186. data/app/pb_kits/playbook/pb_table/styles/_vertical_border.scss +27 -8
  187. data/app/pb_kits/playbook/pb_table/utilities/stickyColumns.test.ts +266 -0
  188. data/app/pb_kits/playbook/pb_table/utilities/stickyColumns.ts +145 -0
  189. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +1 -1
  190. data/app/pb_kits/playbook/pb_text_input/add_on.html.erb +2 -2
  191. data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -1
  192. data/app/pb_kits/playbook/pb_title_count/_title_count.tsx +3 -1
  193. data/app/pb_kits/playbook/pb_title_count/kit.schema.json +1 -0
  194. data/app/pb_kits/playbook/pb_title_detail/_title_detail.tsx +3 -1
  195. data/app/pb_kits/playbook/pb_title_detail/kit.schema.json +1 -0
  196. data/app/pb_kits/playbook/utilities/test/globalProps/onClick.test.js +327 -0
  197. data/dist/ai/all-schemas.json +277 -55
  198. data/dist/ai/forms.json +26 -20
  199. data/dist/ai/index.json +2 -2
  200. data/dist/ai/kits/background.schema.json +1 -0
  201. data/dist/ai/kits/button.schema.json +1 -1
  202. data/dist/ai/kits/caption.schema.json +1 -0
  203. data/dist/ai/kits/checkbox.schema.json +7 -5
  204. data/dist/ai/kits/container.schema.json +1 -0
  205. data/dist/ai/kits/currency.schema.json +1 -0
  206. data/dist/ai/kits/dashboard_value.schema.json +1 -0
  207. data/dist/ai/kits/date_picker.schema.json +4 -3
  208. data/dist/ai/kits/detail.schema.json +1 -0
  209. data/dist/ai/kits/dropdown.schema.json +160 -10
  210. data/dist/ai/kits/highlight.schema.json +1 -0
  211. data/dist/ai/kits/icon_stat_value.schema.json +1 -0
  212. data/dist/ai/kits/icon_value.schema.json +1 -0
  213. data/dist/ai/kits/label_value.schema.json +1 -0
  214. data/dist/ai/kits/legend.schema.json +1 -0
  215. data/dist/ai/kits/message.schema.json +1 -0
  216. data/dist/ai/kits/multi_level_select.schema.json +15 -6
  217. data/dist/ai/kits/multiple_users.schema.json +1 -0
  218. data/dist/ai/kits/multiple_users_stacked.schema.json +1 -0
  219. data/dist/ai/kits/phone_number_input.schema.json +18 -7
  220. data/dist/ai/kits/select.schema.json +25 -6
  221. data/dist/ai/kits/star_rating.schema.json +14 -5
  222. data/dist/ai/kits/stat_value.schema.json +1 -0
  223. data/dist/ai/kits/text_input.schema.json +4 -3
  224. data/dist/ai/kits/textarea.schema.json +4 -3
  225. data/dist/ai/kits/time_picker.schema.json +4 -3
  226. data/dist/ai/kits/title_count.schema.json +1 -0
  227. data/dist/ai/kits/title_detail.schema.json +1 -0
  228. data/dist/ai/kits/typeahead.schema.json +4 -3
  229. data/dist/ai/playgrounds/dropdown.json +1 -1
  230. data/dist/ai/playgrounds/index.json +1 -1
  231. data/dist/ai/playgrounds/map.json +1 -1
  232. data/dist/ai/playgrounds/rich_text_editor.json +1 -1
  233. data/dist/chunks/_pagination-BStnaG2D.js +1 -0
  234. data/dist/chunks/{_pb_line_graph-CUJ7NJIr.js → _pb_line_graph-DAoYQIoh.js} +1 -1
  235. data/dist/chunks/_tooltip-WptzLsCF.js +5 -0
  236. data/dist/chunks/_typeahead-DNfFwTns.js +1 -0
  237. data/dist/chunks/{globalProps-DDcpyjvz.js → globalProps-wXibGgWr.js} +1 -1
  238. data/dist/chunks/lib-BDUQ3wt0.js +29 -0
  239. data/dist/chunks/quickpickDateSync-DLDx4zce.js +1 -0
  240. data/dist/chunks/{time_picker_helper-B_FN8E9Y.js → time_picker_helper-Dc38LjqW.js} +1 -1
  241. data/dist/chunks/vendor.js +2 -2
  242. data/dist/menu.yml +1 -1
  243. data/dist/playbook-rails-react-bindings.js +1 -1
  244. data/dist/playbook-rails.js +1 -1
  245. data/dist/playbook.css +1 -1
  246. data/lib/playbook/forms/builder/checkbox_field.rb +1 -0
  247. data/lib/playbook/forms/builder/collection_select_field.rb +5 -1
  248. data/lib/playbook/forms/builder/select_field.rb +5 -1
  249. data/lib/playbook/version.rb +2 -2
  250. metadata +63 -16
  251. data/dist/chunks/_detail-CDUjsRrc.js +0 -5
  252. data/dist/chunks/_table-C9KHGQhb.js +0 -1
  253. data/dist/chunks/_typeahead-CfxZLhd3.js +0 -1
  254. data/dist/chunks/lib-D57mygNT.js +0 -29
  255. /data/app/pb_kits/playbook/pb_form/docs/{_description.md → _description_rails.md} +0 -0
  256. /data/app/pb_kits/playbook/pb_form/docs/{_form_default_values.md → _form_default_values_rails.md} +0 -0
  257. /data/app/pb_kits/playbook/pb_form/docs/{_form_defaults.md → _form_defaults_rails.md} +0 -0
  258. /data/app/pb_kits/playbook/pb_form/docs/{_form_model_values.md → _form_model_values_rails.md} +0 -0
  259. /data/app/pb_kits/playbook/pb_form/docs/{_form_multi_column.md → _form_multi_column_rails.md} +0 -0
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useEffect, useLayoutEffect, forwardRef, useImperativeHandle, useMemo, useContext } from "react";
1
+ import React, { useState, useRef, useEffect, useLayoutEffect, forwardRef, useImperativeHandle, useMemo, useContext, useCallback } from "react";
2
2
  import classnames from "classnames";
3
3
  import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from "../utilities/props";
4
4
  import { globalProps } from "../utilities/globalProps";
@@ -13,12 +13,14 @@ import DropdownContext from "./context";
13
13
  import DropdownOption from "./subcomponents/DropdownOption";
14
14
  import DropdownTrigger from "./subcomponents/DropdownTrigger";
15
15
  import useDropdown from "./hooks/useDropdown";
16
+ import useAsyncOptions, { LoadOptions } from "./hooks/useAsyncOptions";
16
17
  import getQuickPickOptions from "./quickpick";
17
18
 
18
19
  import {
19
20
  separateChildComponents,
20
21
  prepareSubcomponents,
21
22
  handleClickOutside,
23
+ setQuickPickDateOnPicker,
22
24
  } from "./utilities";
23
25
 
24
26
  import { DialogContext } from "../pb_dialog/_dialog_context";
@@ -81,9 +83,12 @@ function serializeDropdownFilterResetDefault(
81
83
  return undefined;
82
84
  }
83
85
 
86
+ // Async results are server records, so identify them by value like Typeahead rather than by label.
87
+ const asyncOptionValue = (option: GenericObject) => option.value ?? option.id ?? option.label;
88
+
84
89
  type CustomQuickPickDate = {
85
90
  label: string;
86
- value: string[] | { timePeriod: string; amount: number };
91
+ value: Array<Date | string> | { timePeriod: string; amount: number };
87
92
  };
88
93
 
89
94
  type CustomQuickPickDates = {
@@ -91,8 +96,18 @@ type CustomQuickPickDates = {
91
96
  dates: CustomQuickPickDate[];
92
97
  };
93
98
 
99
+ type InputChangeReason = "input" | "clear";
100
+
94
101
  type DropdownProps = {
95
102
  aria?: { [key: string]: string };
103
+ async?: boolean;
104
+ loadOptions?: LoadOptions;
105
+ defaultOptions?: boolean | GenericObject[];
106
+ cacheOptions?: boolean;
107
+ loading?: boolean;
108
+ onInputChange?: (input: string, detail: { reason: InputChangeReason }) => void;
109
+ value?: GenericObject | GenericObject[] | null;
110
+ renderOption?: (option: GenericObject) => React.ReactNode;
96
111
  autocomplete?: boolean;
97
112
  blankSelection?: string;
98
113
  children?: React.ReactChild[] | React.ReactChild | React.ReactElement[];
@@ -113,6 +128,7 @@ type DropdownProps = {
113
128
  label?: string;
114
129
  multiSelect?: boolean;
115
130
  name?: string;
131
+ noOptionsText?: string;
116
132
  onChange?: (event: { target: { name?: string; value: any } }) => void;
117
133
  onSelect?: (arg: GenericObject) => null;
118
134
  options?: GenericObject;
@@ -139,6 +155,14 @@ interface DropdownComponent
139
155
  let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
140
156
  const {
141
157
  aria = {},
158
+ async = false,
159
+ loadOptions,
160
+ defaultOptions = false,
161
+ cacheOptions = false,
162
+ loading = false,
163
+ onInputChange,
164
+ renderOption,
165
+ value,
142
166
  autocomplete = false,
143
167
  blankSelection = '',
144
168
  children,
@@ -159,6 +183,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
159
183
  multiSelect = false,
160
184
  formPillProps,
161
185
  name,
186
+ noOptionsText,
162
187
  onChange,
163
188
  onSelect,
164
189
  options,
@@ -188,15 +213,27 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
188
213
  [classes],
189
214
  );
190
215
 
216
+ const asyncEnabled = async && variant !== "quickpick";
217
+ const getOptionValue = asyncEnabled ? asyncOptionValue : undefined;
218
+ const { options: loadedOptions, status: asyncStatus, search: searchAsync, cancel: cancelAsync, resume: resumeAsync, clear: clearAsync } = useAsyncOptions(asyncEnabled && !!loadOptions && !disabled, loadOptions, defaultOptions, cacheOptions);
219
+
191
220
  // ------------- Quick Pick ---------------------------------
192
221
  // Use QuickPick options when variant is "quickpick"
193
222
  const dropdownOptions = variant === "quickpick"
194
223
  ? getQuickPickOptions(rangeEndsToday, customQuickPickDates)
195
- : (options || []);
224
+ : (asyncEnabled && loadOptions ? loadedOptions : options || []);
196
225
  // ----------------------------------------------------------
197
226
 
198
227
  const [isDropDownClosed, setIsDropDownClosed, toggleDropdown] = useDropdown(disabled ? true : isClosed);
199
228
 
229
+ const previouslyClosed = useRef(isDropDownClosed);
230
+ const queryRef = useRef("");
231
+ useLayoutEffect(() => {
232
+ if (asyncEnabled && (disabled || (!previouslyClosed.current && isDropDownClosed))) cancelAsync();
233
+ if (asyncEnabled && !disabled && previouslyClosed.current && !isDropDownClosed) resumeAsync(queryRef.current);
234
+ previouslyClosed.current = isDropDownClosed;
235
+ }, [asyncEnabled, isDropDownClosed, disabled, cancelAsync, resumeAsync]);
236
+
200
237
  // Use a suffix for the trigger ID to avoid conflict with the outer div's id
201
238
  const sanitizeForId = (str: string) =>
202
239
  str.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
@@ -225,16 +262,53 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
225
262
  return defaultValue || {};
226
263
  }, [multiSelect, defaultValue, variant, dropdownOptions]);
227
264
 
228
- const [selected, setSelected] = useState<GenericObject | GenericObject[]>(
265
+ const [internalSelected, setInternalSelected] = useState<GenericObject | GenericObject[]>(
229
266
  initialSelected
230
267
  );
231
268
 
269
+ const optionKey = (option: GenericObject) => String(
270
+ getOptionValue ? getOptionValue(option) : option.id ?? option.value ?? option.label
271
+ );
272
+
273
+ const isControlled = value !== undefined;
274
+ const selected = isControlled ? (value ?? (multiSelect ? [] : {})) : internalSelected;
275
+ const setSelected: React.Dispatch<React.SetStateAction<GenericObject | GenericObject[]>> = (next) => {
276
+ if (!isControlled) setInternalSelected(next);
277
+ };
278
+ const isSameOption = useCallback((left: GenericObject, right: GenericObject, legacyKey = "label") => {
279
+ if (!getOptionValue) return left?.[legacyKey] === right?.[legacyKey];
280
+ if (!left || !right || !Object.keys(left).length || !Object.keys(right).length) return false;
281
+ return String(getOptionValue(left)) === String(getOptionValue(right));
282
+ }, [getOptionValue]);
283
+
232
284
  // Autocomplete displays the selection in the input; seed from defaultValue
233
285
  const [filterItem, setFilterItem] = useState(() => {
234
286
  if (!autocomplete || multiSelect) return "";
235
- if (Array.isArray(initialSelected)) return "";
236
- return (initialSelected as GenericObject)?.label || "";
287
+ if (Array.isArray(selected)) return "";
288
+ return (selected as GenericObject)?.label || "";
237
289
  });
290
+ queryRef.current = filterItem;
291
+
292
+ const preserveQueryOnControlledClear = useRef(false);
293
+
294
+ // Form adapters may recreate equivalent option objects on every render.
295
+ // Synchronize the query only when selection identity or display text changes.
296
+ const controlledSelectionKey = isControlled ? JSON.stringify(
297
+ (Array.isArray(value) ? value : value && Object.keys(value).length ? [value] : [])
298
+ .map((option) => [
299
+ String(getOptionValue ? getOptionValue(option) : option.id ?? option.value ?? option.label),
300
+ option.label,
301
+ ])
302
+ ) : undefined;
303
+ const controlledLabel = !multiSelect && value && !Array.isArray(value) ? value?.label || "" : "";
304
+
305
+ useEffect(() => {
306
+ if (isControlled && autocomplete) {
307
+ // Accepting a selection clear must not erase the character that started a new search.
308
+ if (controlledLabel || !preserveQueryOnControlledClear.current) setFilterItem(controlledLabel);
309
+ preserveQueryOnControlledClear.current = false;
310
+ }
311
+ }, [isControlled, controlledSelectionKey, controlledLabel, autocomplete, multiSelect]);
238
312
 
239
313
  const filterResetDefaultSerialized = useMemo(
240
314
  () => serializeDropdownFilterResetDefault(variant, multiSelect, defaultValue, dropdownOptions),
@@ -291,6 +365,12 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
291
365
  ? [selected]
292
366
  : [];
293
367
 
368
+ const selectedOptionIds = useMemo(() => (
369
+ multiSelect && getOptionValue
370
+ ? new Set(selectedArray.map((option) => String(getOptionValue(option))))
371
+ : null
372
+ ), [multiSelect, getOptionValue, selectedArray]);
373
+
294
374
  const { trigger, container, otherChildren } =
295
375
  separateChildComponents(children);
296
376
 
@@ -321,33 +401,61 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
321
401
  setIsDropDownClosed(disabled ? true : isClosed)
322
402
  }, [disabled, isClosed])
323
403
 
324
- const blankSelectionOption: GenericObject = blankSelection ? [{ label: blankSelection, value: "" }] : [];
404
+ // blankSelection is a clear action, not a search result, so it cannot stand in for an empty async list.
405
+ const blankSelectionOption: GenericObject = blankSelection && (!asyncEnabled || dropdownOptions.length > 0)
406
+ ? [{ label: blankSelection, value: "" }]
407
+ : [];
325
408
  const optionsWithBlankSelection = blankSelectionOption.concat(dropdownOptions);
326
409
 
327
410
  const availableOptions = useMemo(()=> {
328
411
  if (!multiSelect) return optionsWithBlankSelection;
329
- return optionsWithBlankSelection.filter((option: GenericObject) => !selectedArray.some((sel) => sel.label === option.label));
330
- }, [optionsWithBlankSelection, selectedArray, multiSelect]);
412
+ if (selectedOptionIds) {
413
+ return optionsWithBlankSelection.filter((option: GenericObject) => !selectedOptionIds.has(String(getOptionValue(option))));
414
+ }
415
+ return optionsWithBlankSelection.filter((option: GenericObject) => !selectedArray.some((sel) => isSameOption(sel, option)));
416
+ }, [optionsWithBlankSelection, selectedArray, multiSelect, isSameOption, selectedOptionIds, getOptionValue]);
331
417
 
332
418
  const filteredOptions = useMemo(() => {
333
- // When the input shows the selected label, do not filter the list down to that one option
334
419
  const selectedLabel =
335
420
  !multiSelect &&
336
421
  !Array.isArray(selected) &&
337
422
  (selected as GenericObject)?.label;
423
+ // Async searches stay unfiltered until the input is showing the selected label.
424
+ // Reopening then shows only results that match that text.
425
+ if (asyncEnabled) {
426
+ if (!(autocomplete && selectedLabel && filterItem === selectedLabel)) return availableOptions;
427
+ const query = String(selectedLabel).toLowerCase();
428
+ return availableOptions.filter((opt: GenericObject) => String(opt.label).toLowerCase().includes(query));
429
+ }
430
+ // When the input shows the selected label, do not filter the list down to that one option
338
431
  const filterText =
339
432
  selectedLabel && filterItem === selectedLabel ? "" : filterItem;
340
433
  return availableOptions.filter((opt: GenericObject) =>
341
434
  String(opt.label).toLowerCase().includes(filterText.toLowerCase())
342
435
  );
343
- }, [availableOptions, filterItem, multiSelect, selected]);
436
+ }, [availableOptions, filterItem, multiSelect, selected, asyncEnabled, autocomplete]);
437
+
438
+ // Empty copy follows the settled search. Hiding already-selected rows must not turn a
439
+ // successful result list into "No results found", including the last pick while the menu stays open.
440
+ const asyncEmptyCopy = asyncEnabled && (asyncStatus === "empty" || (asyncStatus === "idle" && dropdownOptions.length === 0))
441
+ ? noOptionsText || "No results found"
442
+ : "";
443
+
444
+ // A keystroke clears the highlight. Focus the first result once the next list arrives,
445
+ // including options the application loads itself.
446
+ const asyncResultOptions = loadOptions ? loadedOptions : options;
447
+ useEffect(() => {
448
+ if (asyncEnabled && !isDropDownClosed) {
449
+ setFocusedOptionIndex(filteredOptions.length ? 0 : -1);
450
+ }
451
+ }, [asyncResultOptions]);
344
452
 
345
453
  // For keyboard accessibility: Set focus within dropdown to selected item if it exists
346
454
  useEffect(() => {
347
455
  if (!isDropDownClosed) {
348
456
  let newIndex = 0;
349
457
  if (selected && !Array.isArray(selected) && selected.label) {
350
- const selectedIndex = filteredOptions.findIndex((option: GenericObject) => option.label === selected.label);
458
+ const selectedIndex = filteredOptions.findIndex((option: GenericObject) => isSameOption(option, selected));
351
459
  if (selectedIndex >= 0) {
352
460
  newIndex = selectedIndex;
353
461
  }
@@ -412,38 +520,70 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
412
520
  ]);
413
521
 
414
522
 
415
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
523
+ const handleSelectionChange = (value: any) => {
524
+ onSelect && onSelect(value);
525
+ onChange && onChange({ target: { name, value } });
526
+ };
527
+
528
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>, replaceSelection = false) => {
416
529
  if (disabled) return;
530
+ if (asyncEnabled) {
531
+ searchAsync(e.target.value);
532
+ setFocusedOptionIndex(-1);
533
+ }
534
+ if ((replaceSelection || ((asyncEnabled || onInputChange) && !e.target.value)) && autocomplete && !multiSelect && selectedArray.length) {
535
+ preserveQueryOnControlledClear.current = isControlled && !!e.target.value;
536
+ setSelected({});
537
+ handleSelectionChange(null);
538
+ }
417
539
  setFilterItem(e.target.value);
540
+ onInputChange?.(e.target.value, { reason: "input" });
418
541
  setIsDropDownClosed(false);
419
542
  };
420
543
 
421
- const handleSelectionChange = (value: any) => {
422
- onSelect && onSelect(value);
423
- onChange && onChange({ target: { name, value } });
424
- };
425
544
 
426
545
  const handleOptionClick = (clickedItem: GenericObject) => {
427
546
  if (disabled) return;
547
+ preserveQueryOnControlledClear.current = false;
548
+ // Single autocomplete keeps the search results so reopening can match the selected label.
549
+ // Other selections still drop back to the initial list.
550
+ if (asyncEnabled) {
551
+ if (!multiSelect && autocomplete) cancelAsync();
552
+ else clearAsync();
553
+ }
428
554
  const shouldCloseOnClick = closeOnClick === "any" || closeOnClick === "inside";
429
555
 
430
556
  if (multiSelect) {
557
+ if (isControlled || getOptionValue) {
558
+ const exists = selectedOptionIds
559
+ ? selectedOptionIds.has(String(getOptionValue(clickedItem)))
560
+ : selectedArray.some((option) => isSameOption(option, clickedItem, "value"));
561
+ const next = exists
562
+ ? selectedArray.filter((option) => !isSameOption(option, clickedItem, "value"))
563
+ : [...selectedArray, clickedItem];
564
+ setSelected(next);
565
+ handleSelectionChange(next);
566
+ } else {
431
567
  setSelected((prev) => {
432
568
  const list = prev as GenericObject[];
433
- const exists = list.find((option) => option.value === clickedItem.value);
569
+ const exists = list.find((option) => isSameOption(option, clickedItem, "value"));
434
570
  const next = exists
435
- ? list.filter((option) => option.value !== clickedItem.value)
571
+ ? list.filter((option) => !isSameOption(option, clickedItem, "value"))
436
572
  : [...list, clickedItem];
437
573
  handleSelectionChange(next);
438
574
  return next;
439
575
  });
576
+ }
440
577
  setFilterItem("");
441
578
  if (shouldCloseOnClick) {
442
579
  setIsDropDownClosed(true);
443
580
  }
444
581
  } else {
445
582
  setSelected(clickedItem);
446
- setFilterItem("");
583
+ // Async autocomplete shows the selection in the input, as controlled mode and Rails do
584
+ setFilterItem(isControlled && autocomplete
585
+ ? (selected as GenericObject)?.label || ""
586
+ : asyncEnabled && autocomplete ? clickedItem.label || "" : "");
447
587
  if (shouldCloseOnClick) {
448
588
  setIsDropDownClosed(true);
449
589
  }
@@ -455,12 +595,12 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
455
595
 
456
596
  if (controlsStartId) {
457
597
  const startPicker = (document.querySelector(`#${controlsStartId}`) as HTMLElement & { _flatpickr?: any })?._flatpickr;
458
- startPicker?.setDate(start, true);
598
+ setQuickPickDateOnPicker(startPicker, start, "start");
459
599
  }
460
600
 
461
601
  if (controlsEndId) {
462
602
  const endPicker = (document.querySelector(`#${controlsEndId}`) as HTMLElement & { _flatpickr?: any })?._flatpickr;
463
- endPicker?.setDate(end, true);
603
+ setQuickPickDateOnPicker(endPicker, end, "end");
464
604
  }
465
605
  }
466
606
  }
@@ -474,6 +614,10 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
474
614
 
475
615
  const handleBackspace = () => {
476
616
  if (disabled) return;
617
+ preserveQueryOnControlledClear.current = false;
618
+ if (asyncEnabled) clearAsync();
619
+ onInputChange?.("", { reason: "clear" });
620
+ if ((onInputChange || asyncEnabled) && multiSelect) setFilterItem("");
477
621
  if (multiSelect) {
478
622
  setSelected([]);
479
623
  handleSelectionChange([]);
@@ -481,7 +625,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
481
625
  setSelected({});
482
626
  handleSelectionChange(null);
483
627
  setFocusedOptionIndex(-1);
484
- setFilterItem("");
628
+ setFilterItem(isControlled && autocomplete && !multiSelect ? (selected as GenericObject)?.label || "" : "");
485
629
 
486
630
  // Clear linked DatePickers as well if this is a quickpick variant with controls
487
631
  if (variant === "quickpick") {
@@ -511,6 +655,9 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
511
655
  // Create an internal ref object that holds the imperative handle methods
512
656
  const imperativeRef = useRef({
513
657
  clearSelected: () => {
658
+ preserveQueryOnControlledClear.current = false;
659
+ if (asyncEnabled) clearAsync();
660
+ onInputChange?.("", { reason: "clear" });
514
661
  if (multiSelect) {
515
662
  setSelected([]);
516
663
  handleSelectionChange([]);
@@ -518,7 +665,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
518
665
  setSelected({});
519
666
  handleSelectionChange(null);
520
667
  }
521
- setFilterItem("");
668
+ setFilterItem(isControlled && autocomplete && !multiSelect ? (selected as GenericObject)?.label || "" : "");
522
669
  setIsDropDownClosed(true);
523
670
  },
524
671
  });
@@ -528,6 +675,9 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
528
675
  useEffect(() => {
529
676
  imperativeRef.current = {
530
677
  clearSelected: () => {
678
+ preserveQueryOnControlledClear.current = false;
679
+ if (asyncEnabled) clearAsync();
680
+ onInputChange?.("", { reason: "clear" });
531
681
  if (multiSelect) {
532
682
  setSelected([]);
533
683
  handleSelectionChange([]);
@@ -535,11 +685,11 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
535
685
  setSelected({});
536
686
  handleSelectionChange(null);
537
687
  }
538
- setFilterItem("");
688
+ setFilterItem(isControlled && autocomplete && !multiSelect ? (selected as GenericObject)?.label || "" : "");
539
689
  setIsDropDownClosed(true);
540
690
  },
541
691
  };
542
- }, [multiSelect, handleSelectionChange, setSelected, setFilterItem, setIsDropDownClosed]);
692
+ }, [multiSelect, handleSelectionChange, setSelected, setFilterItem, setIsDropDownClosed, asyncEnabled, clearAsync, onInputChange]);
543
693
 
544
694
  useImperativeHandle(ref, () => imperativeRef.current);
545
695
 
@@ -562,12 +712,12 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
562
712
  setTimeout(() => {
563
713
  if (controlsStartId) {
564
714
  const startPicker = (document.querySelector(`#${controlsStartId}`) as HTMLElement & { _flatpickr?: any })?._flatpickr;
565
- startPicker?.setDate(start, true);
715
+ setQuickPickDateOnPicker(startPicker, start, "start");
566
716
  }
567
717
 
568
718
  if (controlsEndId) {
569
719
  const endPicker = (document.querySelector(`#${controlsEndId}`) as HTMLElement & { _flatpickr?: any })?._flatpickr;
570
- endPicker?.setDate(end, true);
720
+ setQuickPickDateOnPicker(endPicker, end, "end");
571
721
  }
572
722
  }, 0);
573
723
  }
@@ -579,6 +729,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
579
729
  {...dataProps}
580
730
  {...htmlProps}
581
731
  {...(filterResetDefaultSerialized ? { "data-default-value": filterResetDefaultSerialized } : {})}
732
+ aria-busy={asyncEnabled ? (loading || asyncStatus === "loading") : undefined}
582
733
  className={classes}
583
734
  id={id}
584
735
  ref={outerDivRef}
@@ -586,6 +737,15 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
586
737
  >
587
738
  <DropdownContext.Provider
588
739
  value={{
740
+ asyncEnabled,
741
+ asyncStatus: asyncEnabled ? (loading || asyncStatus === "loading" ? "Loading…" : asyncStatus === "error" ? "Unable to load options" : asyncEmptyCopy) : "",
742
+ renderOption,
743
+ optionKey,
744
+ isControlled,
745
+ onInputChange,
746
+ isSameOption,
747
+ selectedOptionIds,
748
+ getOptionValue,
589
749
  activeStyle,
590
750
  autocomplete,
591
751
  blankSelection,
@@ -613,6 +773,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
613
773
  portalHost,
614
774
  selectId,
615
775
  multiSelect,
776
+ noOptionsText,
616
777
  onSelect,
617
778
  optionsWithBlankSelection,
618
779
  selected,
@@ -682,7 +843,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
682
843
  <DropdownContainer constrainHeight={constrainHeight}>
683
844
  {optionsWithBlankSelection &&
684
845
  optionsWithBlankSelection?.map((option: GenericObject) => (
685
- <DropdownOption key={option.id}
846
+ <DropdownOption key={optionKey(option)}
686
847
  option={option}
687
848
  />
688
849
  ))}
@@ -0,0 +1,58 @@
1
+ <%= pb_rails("dropdown", props: {
2
+ id: "async-rich-user-dropdown",
3
+ label: "User",
4
+ name: "async_rich_user",
5
+ placeholder: "Try Emily or Michael",
6
+ autocomplete: true,
7
+ async: true,
8
+ active_style: { background_color: "bg_light", font_color: "text_lt_default" },
9
+ options: [],
10
+ }) %>
11
+
12
+ <template id="async-user-option-template">
13
+ <%= pb_rails("flex", props: { orientation: "column", align: "start" }) do %>
14
+ <%= pb_rails("body", props: { text: "Name", data: { async_user_name: true } }) %>
15
+ <%= pb_rails("detail", props: { text: "Title and Department", data: { async_user_detail: true } }) %>
16
+ <% end %>
17
+ </template>
18
+
19
+ <script>
20
+ (() => {
21
+ const dropdown = document.getElementById("async-rich-user-dropdown");
22
+ const template = document.getElementById("async-user-option-template");
23
+ if (!dropdown || !template || dropdown.dataset.asyncExampleBound) return;
24
+ dropdown.dataset.asyncExampleBound = "true";
25
+
26
+ dropdown.addEventListener("pb:dropdown:search", (event) => {
27
+ const { searchingFor, setResults, setError } = event.detail;
28
+ const params = new URLSearchParams({
29
+ q: searchingFor,
30
+ limit: "10",
31
+ select: "firstName,lastName,company",
32
+ });
33
+
34
+ fetch(`https://dummyjson.com/users/search?${params}`, { credentials: "omit" })
35
+ .then((response) => {
36
+ if (!response.ok) throw new Error("User search failed");
37
+ return response.json();
38
+ })
39
+ .then(({ users }) => {
40
+ setResults(users.map((user) => {
41
+ const option = {
42
+ id: user.id,
43
+ label: `${user.firstName} ${user.lastName}`,
44
+ value: user.id,
45
+ title: user.company?.title || "",
46
+ department: user.company?.department || "",
47
+ };
48
+ const content = template.content.cloneNode(true);
49
+ content.querySelector("[data-async-user-name]").textContent = option.label;
50
+ content.querySelector("[data-async-user-detail]").textContent =
51
+ [option.title, option.department].filter(Boolean).join(" · ");
52
+ return { option, content };
53
+ }));
54
+ })
55
+ .catch(() => setError());
56
+ });
57
+ })();
58
+ </script>
@@ -0,0 +1,10 @@
1
+ For custom Rails result rows, pass `{ option, content }` entries to the async search event's `setResults` callback. `active_style: { background_color: "bg_light", font_color: "text_lt_default" }` is recommended so the selected row uses a light background and default text color. Plain option objects and rich entries can appear in the same result array.
2
+
3
+ - `option` is the ordinary serializable Dropdown option object: `id`, `label`, `value`, and any custom data. `disabled: true` prevents selection.
4
+ - `content` is an `Element` or `DocumentFragment`, usually cloned from a Rails-rendered `<template>`. Dropdown clones it into its standard option wrapper; it does not consume or move your original nodes. Populate API text with `textContent`. HTML strings are not a rich-content input.
5
+
6
+ The explicit `option.label` is used in the selected input, even when the row contains additional text. Content is not serialized into option data or the existing `pb:dropdown:selected` payload. Nested content supports click selection and keyboard navigation, including when the menu is portaled.
7
+
8
+ Provide display content only: do not include another Dropdown option wrapper, interactive controls, or named form inputs in suggestions. Keep submitted fields outside the result content and update them from selection data. Cloning preserves markup and data attributes but does not copy JavaScript event listeners attached to the original nodes.
9
+
10
+ The example searches [DummyJSON’s public sample users API](https://dummyjson.com/docs/users) without credentials or an API key. Try `Emily` or `Michael`. It renders Rails Body and Detail kits for the name, job title, and department, retaining title and department in the selected option data. The same pattern works with a User kit or other custom display content.
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+ import Dropdown from '../_dropdown'
3
+
4
+ let searchTimer
5
+ let searchController
6
+
7
+ const loadUsers = (term) => new Promise((resolve, reject) => {
8
+ clearTimeout(searchTimer)
9
+ searchController?.abort()
10
+ const controller = searchController = new AbortController()
11
+ searchTimer = setTimeout(() => {
12
+ const params = new URLSearchParams({ q: term, limit: '10', select: 'firstName,lastName' })
13
+ fetch(`https://dummyjson.com/users/search?${params}`, { signal: controller.signal, credentials: 'omit' })
14
+ .then((response) => {
15
+ if (!response.ok) throw new Error('Search failed')
16
+ return response.json()
17
+ })
18
+ .then(({ users }) => resolve(users.map((user) => ({
19
+ ...user,
20
+ label: `${user.firstName} ${user.lastName}`,
21
+ value: user.id,
22
+ }))))
23
+ .catch((error) => {
24
+ if (error.name !== 'AbortError') reject(error)
25
+ })
26
+ }, 250)
27
+ })
28
+
29
+ const DropdownAsyncInitialOptions = () => (
30
+ <Dropdown
31
+ activeStyle={{
32
+ backgroundColor: "bg_light",
33
+ fontColor: "text_lt_default",
34
+ }}
35
+ async
36
+ autocomplete
37
+ cacheOptions
38
+ defaultOptions
39
+ label="User"
40
+ loadOptions={loadUsers}
41
+ placeholder="Try Emily or Michael"
42
+ />
43
+ )
44
+
45
+ export default DropdownAsyncInitialOptions
@@ -0,0 +1,9 @@
1
+ Initial results and caching apply only to `async` Dropdowns with `loadOptions`. `activeStyle={{ backgroundColor: "bg_light", fontColor: "text_lt_default" }}` is recommended so the selected row uses a light background and default text color.
2
+
3
+ - `defaultOptions={true}` calls `loadOptions("")` when enabled without opening the menu.
4
+ - `defaultOptions={options}` supplies an initial result list without making a request. Use `options` without `loadOptions` for externally managed results.
5
+ - `cacheOptions` reuses successful results for exact query strings, including empty results. Each Dropdown keeps at most 100 queries, evicting the oldest stored query. Errors are not cached, and caches are not shared between Dropdowns.
6
+
7
+ Both props default to `false`. When a search scope changes, use React's `key` to remount the scoped Dropdown, or manage results externally. Changing an inline loader function alone does not invalidate cached results.
8
+
9
+ Clearing removes displayed results and invalidates pending work, but retains cached results for subsequent searches. Emptying the query does not automatically restore the initial list.
@@ -0,0 +1,43 @@
1
+ import React from 'react'
2
+ import Dropdown from '../_dropdown'
3
+
4
+ let searchTimer
5
+ let searchController
6
+
7
+ const loadUsers = (term) => new Promise((resolve, reject) => {
8
+ clearTimeout(searchTimer)
9
+ searchController?.abort()
10
+ const controller = searchController = new AbortController()
11
+ searchTimer = setTimeout(() => {
12
+ const params = new URLSearchParams({ q: term, limit: '10', select: 'firstName,lastName' })
13
+ fetch(`https://dummyjson.com/users/search?${params}`, { signal: controller.signal, credentials: 'omit' })
14
+ .then((response) => {
15
+ if (!response.ok) throw new Error('Search failed')
16
+ return response.json()
17
+ })
18
+ .then(({ users }) => resolve(users.map((user) => ({
19
+ ...user,
20
+ label: `${user.firstName} ${user.lastName}`,
21
+ value: user.id,
22
+ }))))
23
+ .catch((error) => {
24
+ if (error.name !== 'AbortError') reject(error)
25
+ })
26
+ }, 250)
27
+ })
28
+
29
+ const DropdownAsyncLoading = () => (
30
+ <Dropdown
31
+ activeStyle={{
32
+ backgroundColor: "bg_light",
33
+ fontColor: "text_lt_default",
34
+ }}
35
+ async
36
+ autocomplete
37
+ label="User"
38
+ loadOptions={loadUsers}
39
+ placeholder="Try Emily or Michael"
40
+ />
41
+ )
42
+
43
+ export default DropdownAsyncLoading
@@ -0,0 +1,9 @@
1
+ Use `async` with `autocomplete` or `Dropdown.Container searchbar` to search remote options. `activeStyle={{ backgroundColor: "bg_light", fontColor: "text_lt_default" }}` is recommended so the selected row uses a light background and default text color. This example queries DummyJSON's public sample users API; the consuming application owns the request and result mapping.
2
+
3
+ `loadOptions(term, callback)` may call `callback(options)` or return a promise resolving to options. Map results to the existing Dropdown shape: `id`, `label`, and `value`, plus any custom fields. Remote matches are displayed without additional local label filtering. When a search returns nothing, earlier results whose labels contain the query stay visible, including text typed after a space. After a single-select autocomplete selection, opening the menu again shows only results whose labels contain the selected label now in the input. With `multiSelect`, each pick becomes a Form Pill, the query clears, and that row stays hidden. See Async Multi Select. Results are matched by `value`, falling back to `id` and then `label`, so results with the same label stay distinct.
4
+
5
+ Dropdown calls the loader for nonempty queries. This example waits 250ms after the last keystroke and aborts the previous request, the same pause Rails uses and the external search example uses. Keep debounce and minimum query length in the application's loader or input handler, as Nitro's existing Typeahead adapters do. No request runs on mount unless `defaultOptions={true}` is supplied.
6
+
7
+ Loading, empty, and error states are built in. Opening the Dropdown before any results are available shows the empty message. Thrown errors, rejected promises, invalid results, and requests exceeding 15 seconds show the error state. Callback loaders should invoke their callback once when complete.
8
+
9
+ New searches, clearing, dismissal, disabling, and unmounting invalidate pending results. Dismissing an unfinished search restores the previous results. A search that already settled as an error or with no results keeps that message when the menu closes, and opening it again does not start another search. Stale responses cannot replace the current results. Application code owns network cancellation. Async mode does not apply to Quick Pick.