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
@@ -370,3 +370,1156 @@ describe("PbDropdown dynamic options", () => {
370
370
  expect(dropdownEl.querySelectorAll(OPTION_SELECTOR).length).toBe(1);
371
371
  });
372
372
  });
373
+
374
+ describe("PbDropdown async search", () => {
375
+ let root;
376
+ let instance;
377
+ let input;
378
+ let search;
379
+
380
+ const type = (term) => {
381
+ input.value = term;
382
+ input.dispatchEvent(new Event("input", { bubbles: true }));
383
+ };
384
+
385
+ beforeEach(() => {
386
+ jest.useFakeTimers();
387
+ root = buildDropdownElement({ options: [] });
388
+ root.dataset.pbDropdownAsync = "true";
389
+ input = document.createElement("input");
390
+ input.setAttribute("data-dropdown-autocomplete", "");
391
+ root.querySelector(".pb_dropdown_trigger").appendChild(input);
392
+ search = jest.fn();
393
+ root.addEventListener("pb:dropdown:search", search);
394
+ instance = new PbDropdown(root);
395
+ instance.connect();
396
+ });
397
+
398
+ afterEach(() => {
399
+ instance.disconnect();
400
+ jest.clearAllTimers();
401
+ jest.useRealTimers();
402
+ document.body.innerHTML = "";
403
+ });
404
+
405
+ test.each([true, false])("option updates retain async results when clearSelection is %p", (clearSelection) => {
406
+ type("old");
407
+ jest.advanceTimersByTime(250);
408
+ const pending = search.mock.calls[0][0].detail;
409
+ document.dispatchEvent(new CustomEvent("pb:dropdown:updateOptions", {
410
+ detail: { dropdownId: root.id, options: [{ id: "new", label: "New option" }], clearSelection },
411
+ }));
412
+ pending.setResults([{ id: "stale", label: "Stale option" }]);
413
+ pending.setError();
414
+ jest.advanceTimersByTime(15000);
415
+ expect(instance.queryAllOptions()).toHaveLength(1);
416
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("New option");
417
+ expect(instance.baseInput.value).toBe("");
418
+ expect(root).toHaveAttribute("aria-busy", "false");
419
+ expect(instance.target.querySelector("[data-dropdown-async-status]")).toBeNull();
420
+ });
421
+
422
+ test("async multi-select reconciliation preserves replacement options", () => {
423
+ instance.disconnect();
424
+ root.dataset.pbDropdownMultiSelect = "true";
425
+ instance.connect();
426
+ instance.replaceOptions([{ id: "new", label: "New option" }], { clearSelection: false });
427
+ expect(instance.queryAllOptions()).toHaveLength(1);
428
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("New option");
429
+ expect(instance.selectedOptions.size).toBe(0);
430
+ });
431
+
432
+ test("requires three characters and debounces typing for 250ms", () => {
433
+ type("ab");
434
+ jest.advanceTimersByTime(250);
435
+ expect(search).not.toHaveBeenCalled();
436
+ expect(instance.target).not.toHaveClass("open");
437
+ type("abc");
438
+ jest.advanceTimersByTime(200);
439
+ type("abcd");
440
+ jest.advanceTimersByTime(249);
441
+ expect(search).not.toHaveBeenCalled();
442
+ jest.advanceTimersByTime(1);
443
+ expect(search).toHaveBeenCalledTimes(1);
444
+ expect(search.mock.calls[0][0].detail.searchingFor).toBe("abcd");
445
+ expect(instance.target).toHaveClass("open");
446
+ expect(root).toHaveAttribute("aria-busy", "true");
447
+ });
448
+
449
+ test("honors configured thresholds and supports the search bar", () => {
450
+ instance.disconnect();
451
+ input.removeAttribute("data-dropdown-autocomplete");
452
+ input.setAttribute("data-dropdown-search", "");
453
+ root.dataset.pbDropdownSearchTermMinimumLength = "1";
454
+ root.dataset.pbDropdownSearchDebounceTimeout = "50";
455
+ instance.connect();
456
+ type("a");
457
+ jest.advanceTimersByTime(50);
458
+ expect(search).toHaveBeenCalledTimes(1);
459
+ });
460
+
461
+ test("renders remote results without local filtering or clearing the query", () => {
462
+ type("abc");
463
+ jest.advanceTimersByTime(250);
464
+ search.mock.calls[0][0].detail.setResults([{ id: "1", label: "Different label" }]);
465
+ expect(input.value).toBe("abc");
466
+ expect(instance.queryAllOptions()).toHaveLength(1);
467
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("Different label");
468
+ expect(instance.queryAllOptions()[0].style.display).toBe("");
469
+ expect(root).toHaveAttribute("aria-busy", "false");
470
+ expect(instance.target.querySelector("[data-dropdown-async-status]")).toBeNull();
471
+ });
472
+
473
+ test("invalidates old callbacks immediately, before the next debounce finishes", () => {
474
+ type("abc");
475
+ jest.advanceTimersByTime(250);
476
+ const first = search.mock.calls[0][0].detail;
477
+ type("abcd");
478
+ first.setResults([{ id: "old", label: "Old" }]);
479
+ expect(instance.queryAllOptions()).toHaveLength(0);
480
+ jest.advanceTimersByTime(250);
481
+ const latest = search.mock.calls[1][0].detail;
482
+ latest.setResults([{ id: "new", label: "New" }]);
483
+ first.setError();
484
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("New");
485
+ latest.setResults([]);
486
+ expect(instance.queryAllOptions()).toHaveLength(1);
487
+ });
488
+
489
+ test("empty results and failed requests end loading with distinct messages", () => {
490
+ type("abc");
491
+ jest.advanceTimersByTime(250);
492
+ search.mock.calls[0][0].detail.setResults([]);
493
+ expect(instance.target.querySelector(".dropdown_no_options")).not.toBeNull();
494
+ expect(root).toHaveAttribute("aria-busy", "false");
495
+ type("def");
496
+ jest.advanceTimersByTime(250);
497
+ search.mock.calls[1][0].detail.setError();
498
+ expect(instance.target).toHaveTextContent("Unable to load options");
499
+ expect(instance.target.querySelector("[data-dropdown-async-status]")).toHaveClass("pb_item_kit", "display_flex", "justify_content_center");
500
+ expect(instance.target.querySelector(".dropdown_no_options")).toBeNull();
501
+ expect(root).toHaveAttribute("aria-busy", "false");
502
+ });
503
+
504
+ test("no_options_text replaces the empty results message", () => {
505
+ instance.disconnect();
506
+ root.dataset.pbDropdownNoOptionsText = "No agents available";
507
+ instance.connect();
508
+ type("abc");
509
+ jest.advanceTimersByTime(250);
510
+ search.mock.calls[0][0].detail.setResults([]);
511
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No agents available");
512
+ });
513
+
514
+ test("unfinished requests time out", () => {
515
+ type("abc");
516
+ jest.advanceTimersByTime(15250);
517
+ expect(root).toHaveAttribute("aria-busy", "false");
518
+ expect(instance.target).toHaveTextContent("Unable to load options");
519
+ });
520
+
521
+ test.each(["short query", "clear", "disconnect"])("%s invalidates pending results", (action) => {
522
+ type("abc");
523
+ jest.advanceTimersByTime(250);
524
+ const request = search.mock.calls[0][0].detail;
525
+ if (action === "short query") type("a");
526
+ if (action === "clear") instance.clearSelection();
527
+ if (action === "disconnect") instance.disconnect();
528
+ request.setResults([{ id: "late", label: "Late" }]);
529
+ expect(instance.queryAllOptions()).toHaveLength(0);
530
+ expect(root).toHaveAttribute("aria-busy", "false");
531
+ });
532
+
533
+ test("events bubble from the kit root and results use the cached portaled container", () => {
534
+ const listener = jest.fn();
535
+ document.addEventListener("pb:dropdown:search", listener);
536
+ const container = instance.target;
537
+ document.body.appendChild(container);
538
+ type("abc");
539
+ jest.advanceTimersByTime(250);
540
+ expect(listener.mock.calls[0][0].target).toBe(root);
541
+ listener.mock.calls[0][0].detail.setResults([{ id: "1", label: "Result" }]);
542
+ expect(container.querySelector(OPTION_SELECTOR)).toHaveTextContent("Result");
543
+ document.removeEventListener("pb:dropdown:search", listener);
544
+ });
545
+
546
+ test("loaded options support keyboard selection and form submission", () => {
547
+ type("can");
548
+ jest.advanceTimersByTime(250);
549
+ search.mock.calls[0][0].detail.setResults([{ id: "ca", label: "Canada", value: "ca" }]);
550
+ const selected = jest.fn();
551
+ root.addEventListener("pb:dropdown:selected", selected);
552
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
553
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
554
+ expect(instance.baseInput.value).toBe("ca");
555
+ expect(input.value).toBe("Canada");
556
+ expect(selected.mock.calls[0][0].detail).toEqual({ id: "ca", label: "Canada", value: "ca" });
557
+ });
558
+
559
+ test("rich results clone template content and keep markup separate from option data", () => {
560
+ const template = document.createElement("template");
561
+ template.innerHTML = '<div data-user-id="42"><strong>Ada</strong></div><span>Engineering</span>';
562
+ const option = { id: "42", label: "Ada", value: "42", department: "Engineering" };
563
+ type("ada");
564
+ jest.advanceTimersByTime(250);
565
+ search.mock.calls[0][0].detail.setResults([
566
+ { option, content: template.content },
567
+ { id: "plain", label: "Plain option" },
568
+ ]);
569
+
570
+ const rows = instance.queryAllOptions();
571
+ expect(rows).toHaveLength(2);
572
+ expect(rows[0].querySelector("[data-user-id]")).toHaveAttribute("data-user-id", "42");
573
+ expect(rows[0].querySelector("span")).toHaveTextContent("Engineering");
574
+ expect(JSON.parse(rows[0].dataset.dropdownOptionLabel)).toEqual(option);
575
+ expect(rows[1]).toHaveTextContent("Plain option");
576
+ expect(template.content.childNodes).toHaveLength(2);
577
+ expect(rows[0].querySelector("strong")).not.toBe(template.content.querySelector("strong"));
578
+
579
+ const selected = jest.fn();
580
+ root.addEventListener("pb:dropdown:selected", selected);
581
+ rows[0].querySelector("strong").click();
582
+ expect(instance.baseInput.value).toBe("42");
583
+ expect(input.value).toBe("Ada");
584
+ expect(selected.mock.calls[0][0].detail).toEqual(option);
585
+ });
586
+
587
+ test("rich Element content works after portaling and disabled rows cannot be selected", () => {
588
+ const content = document.createElement("span");
589
+ content.textContent = "Rich user row";
590
+ const container = instance.target;
591
+ document.body.appendChild(container);
592
+ type("user");
593
+ jest.advanceTimersByTime(250);
594
+ search.mock.calls[0][0].detail.setResults([
595
+ { option: { id: "disabled", label: "Disabled", disabled: true }, content },
596
+ { option: { id: "enabled", label: "Enabled" }, content },
597
+ ]);
598
+
599
+ const rows = instance.queryAllOptions();
600
+ rows[0].querySelector("span").click();
601
+ expect(instance.baseInput.value).toBe("");
602
+ expect(rows[0]).toHaveAttribute("aria-disabled", "true");
603
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
604
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
605
+ expect(instance.baseInput.value).toBe("enabled");
606
+ expect(input.value).toBe("Enabled");
607
+ expect(content.textContent).toBe("Rich user row");
608
+ });
609
+
610
+ test("stale rich results do not replace the current results or consume template content", () => {
611
+ const content = document.createDocumentFragment();
612
+ content.appendChild(document.createElement("span"));
613
+ type("old");
614
+ jest.advanceTimersByTime(250);
615
+ const oldRequest = search.mock.calls[0][0].detail;
616
+ type("new");
617
+ jest.advanceTimersByTime(250);
618
+ search.mock.calls[1][0].detail.setResults([{ id: "new", label: "New" }]);
619
+ oldRequest.setResults([{ option: { id: "old", label: "Old" }, content }]);
620
+ expect(instance.queryAllOptions()).toHaveLength(1);
621
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("New");
622
+ expect(content.childNodes).toHaveLength(1);
623
+ });
624
+
625
+ test("plain result labels are rendered as text", () => {
626
+ type("label");
627
+ jest.advanceTimersByTime(250);
628
+ search.mock.calls[0][0].detail.setResults([{ id: "1", label: "<strong>Label</strong>" }]);
629
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("<strong>Label</strong>");
630
+ expect(instance.queryAllOptions()[0].querySelector("strong")).toBeNull();
631
+ });
632
+
633
+ test.each([42, 0, 'user\'"[42]'])("selection preserves full data for id %p", (id) => {
634
+ const option = {
635
+ id, label: "Ada", value: "employee", department_id: 7,
636
+ address: { city: "Austin", unit: null }, roles: ["admin", "user"], active: false,
637
+ };
638
+ type("ada");
639
+ jest.advanceTimersByTime(250);
640
+ search.mock.calls[0][0].detail.setResults([option]);
641
+ const selected = jest.fn();
642
+ root.addEventListener("pb:dropdown:selected", selected);
643
+ instance.queryAllOptions()[0].click();
644
+ expect(selected).toHaveBeenCalledTimes(1);
645
+ expect(selected.mock.calls[0][0].detail).toEqual(option);
646
+ expect(instance.baseInput.value).toBe(String(id));
647
+ expect(JSON.parse(root.dataset.optionSelected)).toEqual(option);
648
+
649
+ // Event consumers must not mutate the kit's retained selection.
650
+ selected.mock.calls[0][0].detail.address.city = "Changed";
651
+ type("another");
652
+ jest.advanceTimersByTime(250);
653
+ search.mock.calls[1][0].detail.setResults([]);
654
+ instance.emitSelectionChange();
655
+ expect(selected.mock.calls[selected.mock.calls.length - 1][0].detail).toEqual(option);
656
+ instance.clearSelection();
657
+ expect(selected.mock.calls[selected.mock.calls.length - 1][0].detail).toBeNull();
658
+ });
659
+
660
+ test("external selection accepts a string id for a numeric option without changing its payload", () => {
661
+ const option = { id: 42, label: "Ada", value: 42, department_id: 7 };
662
+ type("ada");
663
+ jest.advanceTimersByTime(250);
664
+ search.mock.calls[0][0].detail.setResults([option]);
665
+ const selected = jest.fn();
666
+ root.addEventListener("pb:dropdown:selected", selected);
667
+ document.dispatchEvent(new CustomEvent("pb:dropdown:select", {
668
+ detail: { dropdownId: root.id, optionId: "42" },
669
+ }));
670
+ expect(input.value).toBe("Ada");
671
+ expect(selected.mock.calls[0][0].detail).toEqual(option);
672
+ });
673
+
674
+ test.each([false, true])("async defaults retain their payload without rendered options (multi: %p)", (multi) => {
675
+ instance.disconnect();
676
+ const option = { id: 42, label: "Existing user", value: 42, department_id: 7 };
677
+ root.dataset.pbDropdownMultiSelect = String(multi);
678
+ root.dataset.pbDropdownDefaultValue = JSON.stringify(multi ? [option] : option);
679
+ root.querySelector("[data-dropdown-selected-option]").dataset.defaultValue = "42";
680
+ instance = new PbDropdown(root);
681
+ instance.connect();
682
+ const selected = jest.fn();
683
+ root.addEventListener("pb:dropdown:selected", selected);
684
+ instance.emitSelectionChange();
685
+ expect(instance.queryAllOptions()).toHaveLength(0);
686
+ expect(selected.mock.calls[0][0].detail).toEqual(multi ? [option] : option);
687
+ if (multi) {
688
+ expect(root.querySelector("input[data-generated]").value).toBe("42");
689
+ } else {
690
+ expect(input.value).toBe("Existing user");
691
+ expect(instance.baseInput.value).toBe("42");
692
+ }
693
+ });
694
+
695
+ test("multi-select retains complete selections across searches and matches repeated results by id", () => {
696
+ instance.disconnect();
697
+ root.dataset.pbDropdownMultiSelect = "true";
698
+ const pills = document.createElement("div");
699
+ pills.setAttribute("data-dropdown-pills-wrapper", "");
700
+ root.appendChild(pills);
701
+ instance = new PbDropdown(root);
702
+ instance.connect();
703
+ const first = { id: 0, label: "Ada", value: "not-the-id", department_id: 7 };
704
+ const second = { id: 43, label: "Grace", value: 43, address: { city: "Boston" } };
705
+ const selected = jest.fn();
706
+ root.addEventListener("pb:dropdown:selected", selected);
707
+ type("ada");
708
+ jest.advanceTimersByTime(250);
709
+ search.mock.calls[0][0].detail.setResults([first]);
710
+ instance.queryAllOptions()[0].click();
711
+ expect(root.querySelector("input[data-generated]").value).toBe("0");
712
+
713
+ type("grace");
714
+ jest.advanceTimersByTime(250);
715
+ search.mock.calls[1][0].detail.setResults([{ ...first, label: "Ada updated" }, second]);
716
+ expect(instance.queryAllOptions()[0].style.display).toBe("none");
717
+ instance.queryAllOptions()[1].click();
718
+ expect(selected.mock.calls[selected.mock.calls.length - 1][0].detail).toEqual([first, second]);
719
+ expect(pills.querySelectorAll("[data-pill-id]")).toHaveLength(2);
720
+ expect(instance.queryAllOptions()).toHaveLength(0);
721
+
722
+ pills.querySelector(".pb_form_pill_close").click();
723
+ expect(selected.mock.calls[selected.mock.calls.length - 1][0].detail).toEqual([second]);
724
+ expect(instance.queryAllOptions()).toHaveLength(0);
725
+ });
726
+
727
+ test("input events are immediate, including below the search threshold", () => {
728
+ const changed = jest.fn();
729
+ root.addEventListener("pb:dropdown:input", changed);
730
+ type("a");
731
+ expect(changed).toHaveBeenCalledTimes(1);
732
+ expect(changed.mock.calls[0][0].detail).toEqual({ value: "a", reason: "input" });
733
+ expect(changed.mock.calls[0][0].target).toBe(root);
734
+ expect(search).not.toHaveBeenCalled();
735
+ type("");
736
+ expect(changed.mock.calls[1][0].detail).toEqual({ value: "", reason: "input" });
737
+ });
738
+
739
+ test("clearing typed text invalidates a single selection regardless of edit method", () => {
740
+ type("ada");
741
+ jest.advanceTimersByTime(250);
742
+ search.mock.calls[0][0].detail.setResults([{ id: "42", label: "Ada" }]);
743
+ instance.queryAllOptions()[0].click();
744
+ const selected = jest.fn();
745
+ root.addEventListener("pb:dropdown:selected", selected);
746
+ type("");
747
+ expect(instance.baseInput.value).toBe("");
748
+ expect(selected).toHaveBeenCalledTimes(1);
749
+ expect(selected.mock.calls[0][0].detail).toBeNull();
750
+ expect(instance.queryAllOptions()).toHaveLength(0);
751
+ expect(instance.target).toHaveClass("close");
752
+ });
753
+
754
+ test("query-only clear button and public clear remove results and notify consumers", () => {
755
+ instance.disconnect();
756
+ const clear = document.createElement("button");
757
+ clear.setAttribute("data-dropdown-clear-icon", "");
758
+ root.appendChild(clear);
759
+ instance.connect();
760
+ const changed = jest.fn();
761
+ root.addEventListener("pb:dropdown:input", changed);
762
+ type("ada");
763
+ expect(clear.style.display).toBe("");
764
+ jest.advanceTimersByTime(250);
765
+ const pending = search.mock.calls[0][0].detail;
766
+ clear.click();
767
+ pending.setResults([{ id: "late", label: "Late" }]);
768
+ expect(input.value).toBe("");
769
+ expect(clear.style.display).toBe("none");
770
+ expect(instance.queryAllOptions()).toHaveLength(0);
771
+ expect(changed.mock.calls[changed.mock.calls.length - 1][0].detail).toEqual({ value: "", reason: "clear" });
772
+
773
+ type("new");
774
+ jest.advanceTimersByTime(250);
775
+ search.mock.calls[1][0].detail.setResults([{ id: "new", label: "New" }]);
776
+ document.dispatchEvent(new CustomEvent("pb:dropdown:clear", { detail: { dropdownId: root.id } }));
777
+ expect(instance.queryAllOptions()).toHaveLength(0);
778
+ expect(instance.target).toHaveClass("close");
779
+ expect(root).toHaveAttribute("aria-busy", "false");
780
+ });
781
+
782
+ test("portaled search bar edits and clear events are scoped to the kit root", () => {
783
+ instance.disconnect();
784
+ input.removeAttribute("data-dropdown-autocomplete");
785
+ input.setAttribute("data-dropdown-search", "");
786
+ instance.target.prepend(input);
787
+ instance.connect();
788
+ document.body.appendChild(instance.target);
789
+ const changed = jest.fn();
790
+ root.addEventListener("pb:dropdown:input", changed);
791
+ type("ada");
792
+ expect(changed.mock.calls[0][0].target).toBe(root);
793
+ instance.clearSelection();
794
+ expect(input.value).toBe("");
795
+ expect(changed.mock.calls[1][0].detail).toEqual({ value: "", reason: "clear" });
796
+ });
797
+
798
+ test("native form reset clears results and callbacks, while canceled reset leaves state intact", async () => {
799
+ instance.disconnect();
800
+ const form = document.createElement("form");
801
+ document.body.appendChild(form);
802
+ form.appendChild(root);
803
+ instance.connect();
804
+ const changed = jest.fn();
805
+ root.addEventListener("pb:dropdown:input", changed);
806
+ type("ada");
807
+ jest.advanceTimersByTime(250);
808
+ const pending = search.mock.calls[0][0].detail;
809
+ const preventReset = (event) => event.preventDefault();
810
+ form.addEventListener("reset", preventReset);
811
+ form.reset();
812
+ await Promise.resolve();
813
+ expect(input.value).toBe("ada");
814
+ expect(root).toHaveAttribute("aria-busy", "true");
815
+ form.removeEventListener("reset", preventReset);
816
+ form.reset();
817
+ await Promise.resolve();
818
+ pending.setResults([{ id: "late", label: "Late" }]);
819
+ expect(input.value).toBe("");
820
+ expect(instance.queryAllOptions()).toHaveLength(0);
821
+ expect(root).toHaveAttribute("aria-busy", "false");
822
+ expect(changed.mock.calls[changed.mock.calls.length - 1][0].detail).toEqual({ value: "", reason: "reset" });
823
+ });
824
+
825
+ test("reconnecting does not duplicate form reset listeners and disconnect cancels queued resets", async () => {
826
+ instance.disconnect();
827
+ const form = document.createElement("form");
828
+ document.body.appendChild(form);
829
+ form.appendChild(root);
830
+ instance.connect();
831
+ instance.disconnect();
832
+ instance.connect();
833
+ const changed = jest.fn();
834
+ root.addEventListener("pb:dropdown:input", changed);
835
+ form.reset();
836
+ await Promise.resolve();
837
+ expect(changed).toHaveBeenCalledTimes(1);
838
+ form.reset();
839
+ instance.disconnect();
840
+ await Promise.resolve();
841
+ expect(changed).toHaveBeenCalledTimes(1);
842
+ });
843
+
844
+ test("editing a multi-select query does not clear selected pills", () => {
845
+ instance.disconnect();
846
+ root.dataset.pbDropdownMultiSelect = "true";
847
+ instance.connect();
848
+ type("ada");
849
+ jest.advanceTimersByTime(250);
850
+ search.mock.calls[0][0].detail.setResults([{ id: "42", label: "Ada" }]);
851
+ instance.queryAllOptions()[0].click();
852
+ type("new");
853
+ type("");
854
+ expect(Array.from(instance.selectedOptions).map(JSON.parse)).toEqual([{ id: "42", label: "Ada", value: "42" }]);
855
+ expect(root.querySelector("input[data-generated]").value).toBe("42");
856
+ });
857
+
858
+ test("clear followed by immediate retyping accepts only the latest response", () => {
859
+ root.dataset.pbDropdownSearchDebounceTimeout = "0";
860
+ type("old");
861
+ jest.advanceTimersByTime(0);
862
+ const previous = search.mock.calls[0][0].detail;
863
+ instance.clearSelection();
864
+ type("new");
865
+ jest.advanceTimersByTime(0);
866
+ previous.setResults([{ id: "old", label: "Old" }]);
867
+ search.mock.calls[1][0].detail.setResults([{ id: "new", label: "New" }]);
868
+ expect(instance.target).toHaveClass("open");
869
+ expect(instance.queryAllOptions()).toHaveLength(1);
870
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("New");
871
+ });
872
+
873
+ test("clearing the search bar preserves the selection", () => {
874
+ instance.disconnect();
875
+ input.removeAttribute("data-dropdown-autocomplete");
876
+ input.setAttribute("data-dropdown-search", "");
877
+ instance.connect();
878
+ type("ada");
879
+ jest.advanceTimersByTime(250);
880
+ search.mock.calls[0][0].detail.setResults([{ id: "42", label: "Ada" }]);
881
+ instance.queryAllOptions()[0].click();
882
+ type("");
883
+ expect(instance.baseInput.value).toBe("42");
884
+ });
885
+
886
+ test("clearable false still hides the clear control while typing", () => {
887
+ instance.disconnect();
888
+ root.dataset.pbDropdownClearable = "false";
889
+ const clear = document.createElement("button");
890
+ clear.setAttribute("data-dropdown-clear-icon", "");
891
+ root.appendChild(clear);
892
+ instance.connect();
893
+ type("ada");
894
+ expect(clear.style.display).toBe("none");
895
+ });
896
+
897
+ test("a selection handler can publicly clear an async picker without keyboard selection reopening it", () => {
898
+ root.addEventListener("pb:dropdown:selected", ({ detail }) => {
899
+ if (detail) {
900
+ document.dispatchEvent(new CustomEvent("pb:dropdown:clear", { detail: { dropdownId: root.id } }));
901
+ }
902
+ });
903
+ type("ada");
904
+ jest.advanceTimersByTime(250);
905
+ search.mock.calls[0][0].detail.setResults([{ id: "42", label: "Ada" }]);
906
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
907
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
908
+ expect(input.value).toBe("");
909
+ expect(instance.baseInput.value).toBe("");
910
+ expect(instance.queryAllOptions()).toHaveLength(0);
911
+ expect(instance.target).toHaveClass("close");
912
+ });
913
+
914
+ test.each(["click", "ArrowDown", "Enter"])("%s opens an empty async menu with the empty message", (action) => {
915
+ const down = document.createElement("span");
916
+ down.setAttribute("data-dropdown-open-icon", "");
917
+ const up = document.createElement("span");
918
+ up.setAttribute("data-dropdown-close-icon", "");
919
+ root.append(down, up);
920
+ type("ada");
921
+ jest.advanceTimersByTime(250);
922
+ search.mock.calls[0][0].detail.setResults([{ id: "42", label: "Ada" }]);
923
+ instance.queryAllOptions()[0].click();
924
+ instance.clearSelection();
925
+ jest.advanceTimersByTime(0);
926
+ if (action === "click") input.click();
927
+ else input.dispatchEvent(new KeyboardEvent("keydown", { key: action, bubbles: true }));
928
+ expect(instance.target).toHaveClass("open");
929
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No results found");
930
+ expect(down.style.display).toBe("none");
931
+ expect(up.style.display).not.toBe("none");
932
+ type("new");
933
+ jest.advanceTimersByTime(250);
934
+ expect(instance.target).toHaveClass("open");
935
+ expect(instance.target).toHaveTextContent("Loading");
936
+ expect(instance.target.querySelector(".dropdown_no_options")).toBeNull();
937
+ });
938
+
939
+ test("a shorter query after a selection shows sibling matches the selected label had hidden", () => {
940
+ type("Emily");
941
+ jest.advanceTimersByTime(250);
942
+ search.mock.calls[0][0].detail.setResults([
943
+ { id: "1", label: "Emily Smith" },
944
+ { id: "2", label: "Emily Sanders" },
945
+ { id: "3", label: "Emily Jones" },
946
+ ]);
947
+ instance.queryAllOptions()[0].click();
948
+ jest.advanceTimersByTime(0);
949
+
950
+ type("Emily S");
951
+ jest.advanceTimersByTime(250);
952
+ search.mock.calls[1][0].detail.setResults([]);
953
+
954
+ const rows = () => Array.from(instance.queryAllOptions());
955
+ const visible = () => rows().filter((opt) => opt.style.display !== "none");
956
+ expect(rows().map((opt) => opt.textContent)).toEqual(["Emily Smith", "Emily Sanders"]);
957
+ expect(visible().map((opt) => opt.textContent)).toEqual(["Emily Smith", "Emily Sanders"]);
958
+ expect(instance.target.querySelector(".dropdown_no_options")).toBeNull();
959
+ });
960
+
961
+ test("a query after a space keeps earlier results whose labels match", () => {
962
+ type("Emily");
963
+ jest.advanceTimersByTime(250);
964
+ search.mock.calls[0][0].detail.setResults([
965
+ { id: "1", label: "Emily Smith" },
966
+ { id: "2", label: "Emily Jones" },
967
+ ]);
968
+ type("Emily S");
969
+ jest.advanceTimersByTime(250);
970
+ search.mock.calls[1][0].detail.setResults([]);
971
+ const labels = () => Array.from(instance.queryAllOptions()).map((opt) => opt.textContent);
972
+ expect(labels()).toEqual(["Emily Smith"]);
973
+ expect(instance.target.querySelector(".dropdown_no_options")).toBeNull();
974
+
975
+ type("Emily Z");
976
+ jest.advanceTimersByTime(250);
977
+ search.mock.calls[2][0].detail.setResults([]);
978
+ expect(instance.queryAllOptions()).toHaveLength(0);
979
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No results found");
980
+ });
981
+
982
+ test("active_style classes are applied to async results", () => {
983
+ instance.disconnect();
984
+ root.dataset.pbDropdownActiveStyle = JSON.stringify({
985
+ background_color: "bg_light",
986
+ font_color: "text_lt_default",
987
+ });
988
+ instance.connect();
989
+ type("ada");
990
+ jest.advanceTimersByTime(250);
991
+ search.mock.calls[0][0].detail.setResults([{ id: "1", label: "Ada Lovelace" }]);
992
+ const option = instance.queryAllOptions()[0];
993
+ expect(option).toHaveClass("bg-bg_light", "font-text_lt_default");
994
+ });
995
+
996
+ test("reopening a multi-select after a pick shows the empty message", () => {
997
+ instance.disconnect();
998
+ root.dataset.pbDropdownMultiSelect = "true";
999
+ const pills = document.createElement("div");
1000
+ pills.setAttribute("data-dropdown-pills-wrapper", "");
1001
+ root.appendChild(pills);
1002
+ instance = new PbDropdown(root);
1003
+ instance.connect();
1004
+
1005
+ type("Emily");
1006
+ jest.advanceTimersByTime(250);
1007
+ search.mock.calls[0][0].detail.setResults([
1008
+ { id: "1", label: "Emily Smith" },
1009
+ { id: "2", label: "Emily Jones" },
1010
+ ]);
1011
+ instance.queryAllOptions()[0].click();
1012
+ jest.advanceTimersByTime(0);
1013
+ expect(instance.queryAllOptions()).toHaveLength(0);
1014
+ input.click();
1015
+ expect(instance.target).toHaveClass("open");
1016
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No results found");
1017
+ });
1018
+
1019
+ test("removing the last multi-select pill drops the earlier search results", () => {
1020
+ instance.disconnect();
1021
+ root.dataset.pbDropdownMultiSelect = "true";
1022
+ const pills = document.createElement("div");
1023
+ pills.setAttribute("data-dropdown-pills-wrapper", "");
1024
+ root.appendChild(pills);
1025
+ instance = new PbDropdown(root);
1026
+ instance.connect();
1027
+
1028
+ type("Emily");
1029
+ jest.advanceTimersByTime(250);
1030
+ search.mock.calls[0][0].detail.setResults([
1031
+ { id: "1", label: "Emily Smith" },
1032
+ { id: "2", label: "Emily Jones" },
1033
+ ]);
1034
+ instance.queryAllOptions()[0].click();
1035
+ jest.advanceTimersByTime(0);
1036
+ type("Michael");
1037
+ jest.advanceTimersByTime(250);
1038
+ search.mock.calls[1][0].detail.setResults([{ id: "3", label: "Michael Scott" }]);
1039
+ expect(instance.queryAllOptions()).toHaveLength(1);
1040
+ input.value = "";
1041
+
1042
+ pills.querySelector(".pb_form_pill_close").click();
1043
+ expect(instance.queryAllOptions()).toHaveLength(0);
1044
+
1045
+ input.click();
1046
+ expect(instance.target).toHaveClass("open");
1047
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No results found");
1048
+ });
1049
+
1050
+ test("a default selection marks the matching search row so reopening narrows to that label", () => {
1051
+ instance.disconnect();
1052
+ const option = { id: 42, label: "Ada Lovelace", value: 42 };
1053
+ root.dataset.pbDropdownDefaultValue = JSON.stringify(option);
1054
+ root.querySelector("[data-dropdown-selected-option]").dataset.defaultValue = "42";
1055
+ instance = new PbDropdown(root);
1056
+ instance.connect();
1057
+ expect(input.value).toBe("Ada Lovelace");
1058
+
1059
+ type("ada");
1060
+ jest.advanceTimersByTime(250);
1061
+ search.mock.calls[0][0].detail.setResults([
1062
+ { id: 42, label: "Ada Lovelace" },
1063
+ { id: 7, label: "Ada Lovelace Jr" },
1064
+ { id: 8, label: "Canada" },
1065
+ ]);
1066
+ const rows = () => Array.from(instance.queryAllOptions());
1067
+ const visible = () => rows().filter((opt) => opt.style.display !== "none");
1068
+ expect(rows()[0]).toHaveClass("pb_dropdown_option_selected");
1069
+ expect(visible()).toHaveLength(3);
1070
+
1071
+ input.value = "Ada Lovelace";
1072
+ instance.hideElement(instance.target);
1073
+ jest.advanceTimersByTime(0);
1074
+ input.click();
1075
+ expect(instance.target).toHaveClass("open");
1076
+ expect(visible().map((opt) => opt.textContent)).toEqual(["Ada Lovelace", "Ada Lovelace Jr"]);
1077
+ });
1078
+
1079
+ test("reopening after a selection shows only the option matching the input", () => {
1080
+ type("ada");
1081
+ jest.advanceTimersByTime(250);
1082
+ search.mock.calls[0][0].detail.setResults([
1083
+ { id: "1", label: "Ada Lovelace" },
1084
+ { id: "2", label: "Adam Smith" },
1085
+ ]);
1086
+ instance.queryAllOptions()[0].click();
1087
+ jest.advanceTimersByTime(0);
1088
+ expect(input.value).toBe("Ada Lovelace");
1089
+ expect(instance.target).toHaveClass("close");
1090
+ input.click();
1091
+ const visible = () => Array.from(instance.queryAllOptions()).filter((opt) => opt.style.display !== "none");
1092
+ expect(instance.target).toHaveClass("open");
1093
+ expect(visible()).toHaveLength(1);
1094
+ expect(visible()[0]).toHaveTextContent("Ada Lovelace");
1095
+ expect(instance.target.querySelector(".dropdown_no_options")).toBeNull();
1096
+ expect(search).toHaveBeenCalledTimes(1);
1097
+
1098
+ instance.target.style.height = "400px";
1099
+ Object.defineProperty(instance.target, "scrollHeight", {
1100
+ configurable: true,
1101
+ get() {
1102
+ return this.style.height === "auto" ? 48 : parseFloat(this.style.height) || 0;
1103
+ },
1104
+ });
1105
+ instance.hideElement(instance.target);
1106
+ jest.advanceTimersByTime(0);
1107
+ input.click();
1108
+ expect(instance.target.style.height).toBe("48px");
1109
+ });
1110
+
1111
+ test("clicking an empty async dropdown shows the empty message until a search starts", () => {
1112
+ input.click();
1113
+ expect(instance.target).toHaveClass("open");
1114
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No results found");
1115
+
1116
+ type("abc");
1117
+ expect(instance.target).toHaveClass("open");
1118
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No results found");
1119
+ jest.advanceTimersByTime(250);
1120
+ expect(instance.target).toHaveTextContent("Loading");
1121
+ expect(instance.target.querySelector(".dropdown_no_options")).toBeNull();
1122
+ search.mock.calls[0][0].detail.setResults([{ id: "1", label: "Result" }]);
1123
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("Result");
1124
+ expect(instance.target.querySelector(".dropdown_no_options")).toBeNull();
1125
+ });
1126
+
1127
+ test("no_options_text is the empty message shown before an async search", () => {
1128
+ instance.disconnect();
1129
+ root.dataset.pbDropdownNoOptionsText = "No agents available";
1130
+ instance.connect();
1131
+ input.click();
1132
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No agents available");
1133
+ });
1134
+
1135
+ test("an empty async search-bar menu can still open so the user can type", () => {
1136
+ instance.disconnect();
1137
+ input.removeAttribute("data-dropdown-autocomplete");
1138
+ input.setAttribute("data-dropdown-search", "");
1139
+ instance.target.prepend(input);
1140
+ instance.connect();
1141
+ instance.clearSelection();
1142
+ root.querySelector(".pb_dropdown_trigger").click();
1143
+ expect(instance.target).toHaveClass("open");
1144
+ expect(instance.target.querySelector(".dropdown_no_options")).toHaveTextContent("No results found");
1145
+ });
1146
+
1147
+ test.each(["empty", "error"])("async %s status can be reopened", (status) => {
1148
+ type("ada");
1149
+ jest.advanceTimersByTime(250);
1150
+ const request = search.mock.calls[0][0].detail;
1151
+ if (status === "empty") request.setResults([]);
1152
+ else request.setError();
1153
+ instance.hideElement(instance.target);
1154
+ jest.advanceTimersByTime(0);
1155
+ input.click();
1156
+ expect(instance.target).toHaveClass("open");
1157
+ expect(instance.target).toHaveTextContent(status === "empty" ? "No results found" : "Unable to load options");
1158
+ });
1159
+
1160
+ test.each(["Escape", "Tab", "outside click"])("%s cancels debounce even before the menu opens", (action) => {
1161
+ type("ada");
1162
+ if (action === "outside click") document.body.click();
1163
+ else input.dispatchEvent(new KeyboardEvent("keydown", { key: action, bubbles: true }));
1164
+ jest.advanceTimersByTime(250);
1165
+ expect(search).not.toHaveBeenCalled();
1166
+ expect(instance.target).not.toHaveClass("open");
1167
+ expect(root).toHaveAttribute("aria-busy", "false");
1168
+
1169
+ type("new");
1170
+ jest.advanceTimersByTime(250);
1171
+ expect(search).toHaveBeenCalledTimes(1);
1172
+ expect(instance.target).toHaveClass("open");
1173
+ });
1174
+
1175
+ test.each(["Escape", "Tab", "outside click"])("%s during debounce keeps loaded results and reopening retries the query", (action) => {
1176
+ type("ada");
1177
+ jest.advanceTimersByTime(250);
1178
+ search.mock.calls[0][0].detail.setResults([{ id: "42", label: "Ada" }]);
1179
+ type("adal");
1180
+ expect(instance.queryAllOptions()).toHaveLength(1);
1181
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("Ada");
1182
+ if (action === "outside click") document.body.click();
1183
+ else input.dispatchEvent(new KeyboardEvent("keydown", { key: action, bubbles: true }));
1184
+ jest.advanceTimersByTime(0);
1185
+ expect(search).toHaveBeenCalledTimes(1);
1186
+ expect(instance.target).toHaveClass("close");
1187
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("Ada");
1188
+
1189
+ input.click();
1190
+ expect(instance.target).toHaveClass("open");
1191
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("Ada");
1192
+ jest.advanceTimersByTime(250);
1193
+ expect(search).toHaveBeenCalledTimes(2);
1194
+ expect(search.mock.calls[1][0].detail.searchingFor).toBe("adal");
1195
+ expect(instance.target).toHaveTextContent("Loading");
1196
+ });
1197
+
1198
+ test.each(["Escape", "Tab", "outside click"])("%s during an in-flight search restores loaded results and reopening retries", (action) => {
1199
+ type("ada");
1200
+ jest.advanceTimersByTime(250);
1201
+ search.mock.calls[0][0].detail.setResults([{ id: "42", label: "Ada" }]);
1202
+ type("adal");
1203
+ jest.advanceTimersByTime(250);
1204
+ const pending = search.mock.calls[1][0].detail;
1205
+ expect(instance.queryAllOptions()).toHaveLength(0);
1206
+ if (action === "outside click") document.body.click();
1207
+ else input.dispatchEvent(new KeyboardEvent("keydown", { key: action, bubbles: true }));
1208
+ pending.setResults([{ id: "late", label: "Late" }]);
1209
+ jest.advanceTimersByTime(0);
1210
+ expect(instance.target).toHaveClass("close");
1211
+ expect(instance.queryAllOptions()).toHaveLength(1);
1212
+ expect(instance.queryAllOptions()[0]).toHaveTextContent("Ada");
1213
+ expect(instance.target).not.toHaveTextContent("Late");
1214
+
1215
+ input.click();
1216
+ jest.advanceTimersByTime(250);
1217
+ expect(search).toHaveBeenCalledTimes(3);
1218
+ expect(search.mock.calls[2][0].detail.searchingFor).toBe("adal");
1219
+ expect(instance.target).toHaveClass("open");
1220
+ expect(instance.target).toHaveTextContent("Loading");
1221
+ });
1222
+
1223
+ test("reopening after a dismissed debounce retries a nonempty autocomplete query", () => {
1224
+ type("ada");
1225
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
1226
+ jest.advanceTimersByTime(250);
1227
+ expect(search).not.toHaveBeenCalled();
1228
+ expect(instance.target).not.toHaveClass("open");
1229
+
1230
+ input.click();
1231
+ jest.advanceTimersByTime(250);
1232
+ expect(search).toHaveBeenCalledTimes(1);
1233
+ expect(search.mock.calls[0][0].detail.searchingFor).toBe("ada");
1234
+ expect(instance.target).toHaveClass("open");
1235
+ expect(instance.target).toHaveTextContent("Loading");
1236
+ });
1237
+
1238
+ test.each(["Escape", "Tab", "outside click"])("%s invalidates in-flight callbacks and their timeout", (action) => {
1239
+ type("ada");
1240
+ jest.advanceTimersByTime(250);
1241
+ const pending = search.mock.calls[0][0].detail;
1242
+ if (action === "outside click") document.body.click();
1243
+ else input.dispatchEvent(new KeyboardEvent("keydown", { key: action, bubbles: true }));
1244
+ pending.setResults([{ id: "late", label: "Late" }]);
1245
+ pending.setError();
1246
+ jest.advanceTimersByTime(15000);
1247
+ expect(instance.queryAllOptions()).toHaveLength(0);
1248
+ expect(instance.target).not.toHaveClass("open");
1249
+ expect(instance.target.querySelector("[data-dropdown-async-status]")).toBeNull();
1250
+ expect(root).toHaveAttribute("aria-busy", "false");
1251
+ });
1252
+
1253
+ test("clicking another Dropdown search bar dismisses a pending search", () => {
1254
+ const otherInput = document.createElement("input");
1255
+ otherInput.setAttribute("data-dropdown-search", "");
1256
+ document.body.appendChild(otherInput);
1257
+ type("ada");
1258
+ otherInput.click();
1259
+ jest.advanceTimersByTime(250);
1260
+ expect(search).not.toHaveBeenCalled();
1261
+ expect(instance.target).not.toHaveClass("open");
1262
+ });
1263
+
1264
+ test.each([false, true])("search-bar typing stays open below the minimum (portaled: %p)", (portaled) => {
1265
+ instance.disconnect();
1266
+ input.removeAttribute("data-dropdown-autocomplete");
1267
+ input.setAttribute("data-dropdown-search", "");
1268
+ instance.target.prepend(input);
1269
+ instance.connect();
1270
+ instance.showElement(instance.target);
1271
+ if (portaled) document.body.appendChild(instance.target);
1272
+ input.focus();
1273
+
1274
+ for (const term of ["a", "ad"]) {
1275
+ type(term);
1276
+ jest.advanceTimersByTime(250);
1277
+ expect(instance.target).toHaveClass("open");
1278
+ expect(instance.target).not.toHaveClass("close");
1279
+ expect(document.activeElement).toBe(input);
1280
+ expect(search).not.toHaveBeenCalled();
1281
+ }
1282
+ type("ada");
1283
+ jest.advanceTimersByTime(250);
1284
+ expect(search).toHaveBeenCalledTimes(1);
1285
+ expect(search.mock.calls[0][0].detail.searchingFor).toBe("ada");
1286
+ });
1287
+
1288
+ test.each([false, true])("deleting a search-bar query clears results but retains the input (portaled: %p)", (portaled) => {
1289
+ instance.disconnect();
1290
+ input.removeAttribute("data-dropdown-autocomplete");
1291
+ input.setAttribute("data-dropdown-search", "");
1292
+ instance.target.prepend(input);
1293
+ instance.connect();
1294
+ instance.showElement(instance.target);
1295
+ if (portaled) document.body.appendChild(instance.target);
1296
+ input.focus();
1297
+ type("ada");
1298
+ jest.advanceTimersByTime(250);
1299
+ search.mock.calls[0][0].detail.setResults([{ id: "42", label: "Ada" }]);
1300
+ type("ad");
1301
+ expect(instance.queryAllOptions()).toHaveLength(0);
1302
+ expect(instance.target).toHaveClass("open");
1303
+ expect(document.activeElement).toBe(input);
1304
+
1305
+ type("adam");
1306
+ jest.advanceTimersByTime(250);
1307
+ const pending = search.mock.calls[1][0].detail;
1308
+ type("");
1309
+ pending.setResults([{ id: "late", label: "Late" }]);
1310
+ pending.setError();
1311
+ jest.advanceTimersByTime(15000);
1312
+ expect(instance.queryAllOptions()).toHaveLength(0);
1313
+ expect(instance.target).toHaveClass("open");
1314
+ expect(document.activeElement).toBe(input);
1315
+ expect(root).toHaveAttribute("aria-busy", "false");
1316
+ expect(instance.target.querySelector("[data-dropdown-async-status]")).toBeNull();
1317
+ expect(search).toHaveBeenCalledTimes(2);
1318
+ });
1319
+
1320
+ test("disabled dropdowns do not search", () => {
1321
+ instance.isDisabled = true;
1322
+ type("abc");
1323
+ jest.advanceTimersByTime(250);
1324
+ expect(search).not.toHaveBeenCalled();
1325
+ });
1326
+ });
1327
+
1328
+
1329
+ describe("PbDropdown synchronous compatibility", () => {
1330
+ let root;
1331
+ let instance;
1332
+ let input;
1333
+
1334
+ beforeEach(() => {
1335
+ root = buildDropdownElement();
1336
+ input = document.createElement("input");
1337
+ input.setAttribute("data-dropdown-autocomplete", "");
1338
+ root.querySelector(".pb_dropdown_trigger").appendChild(input);
1339
+ const form = document.createElement("form");
1340
+ document.body.appendChild(form);
1341
+ form.appendChild(root);
1342
+ instance = new PbDropdown(root);
1343
+ instance.connect();
1344
+ });
1345
+
1346
+ afterEach(() => {
1347
+ instance.disconnect();
1348
+ document.body.innerHTML = "";
1349
+ });
1350
+
1351
+ test("local filtering is immediate and emits no async events", () => {
1352
+ const changed = jest.fn();
1353
+ root.addEventListener("pb:dropdown:input", changed);
1354
+ root.addEventListener("pb:dropdown:search", changed);
1355
+ input.value = "can";
1356
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1357
+ expect(instance.queryAllOptions()[0].style.display).toBe("none");
1358
+ expect(instance.queryAllOptions()[1].style.display).toBe("");
1359
+ expect(changed).not.toHaveBeenCalled();
1360
+ expect(root).not.toHaveAttribute("aria-busy");
1361
+ });
1362
+
1363
+ test("selection retains legacy notification timing and payload, and clear keeps static options", () => {
1364
+ const selected = jest.fn(() => {
1365
+ expect(instance.target).not.toHaveClass("close");
1366
+ });
1367
+ instance.target.classList.remove("close");
1368
+ instance.target.classList.add("open");
1369
+ root.addEventListener("pb:dropdown:selected", selected);
1370
+ instance.queryAllOptions()[0].click();
1371
+ // The legacy composite trigger emits once per display (text and autocomplete).
1372
+ expect(selected).toHaveBeenCalledTimes(2);
1373
+ expect(selected.mock.calls[0][0].detail).toEqual({ id: "us", label: "United States", value: "us" });
1374
+ root.removeEventListener("pb:dropdown:selected", selected);
1375
+ const changed = jest.fn();
1376
+ root.addEventListener("pb:dropdown:input", changed);
1377
+ instance.clearSelection();
1378
+ expect(instance.queryAllOptions()).toHaveLength(2);
1379
+ expect(changed).not.toHaveBeenCalled();
1380
+ expect(root).not.toHaveAttribute("aria-busy");
1381
+ });
1382
+
1383
+ test("Quick Pick still updates and clears its date fields", () => {
1384
+ root.dataset.pbDropdownVariant = "quickpick";
1385
+ root.dataset.startDateId = "compat-start";
1386
+ root.dataset.endDateId = "compat-end";
1387
+ const start = document.createElement("input");
1388
+ start.id = "compat-start";
1389
+ const end = document.createElement("input");
1390
+ end.id = "compat-end";
1391
+ root.append(start, end);
1392
+ const option = {
1393
+ id: "today", label: "Today", value: "today",
1394
+ formatted_start_date: "09/29/2026", formatted_end_date: "09/29/2026",
1395
+ };
1396
+ instance.replaceOptions([option]);
1397
+ instance.queryAllOptions()[0].click();
1398
+ expect(start.value).toBe("09/29/2026");
1399
+ expect(end.value).toBe("09/29/2026");
1400
+ expect(JSON.parse(root.dataset.optionSelected)).toEqual(option);
1401
+ instance.clearSelection();
1402
+ expect(start.value).toBe("");
1403
+ expect(end.value).toBe("");
1404
+ expect(instance.queryAllOptions()).toHaveLength(1);
1405
+ });
1406
+
1407
+ test("native reset still clears synchronously without selection or input notifications", () => {
1408
+ instance.setSelectionByOptionId("ca");
1409
+ const changed = jest.fn();
1410
+ root.addEventListener("pb:dropdown:input", changed);
1411
+ root.addEventListener("pb:dropdown:selected", changed);
1412
+ root.closest("form").reset();
1413
+ expect(instance.baseInput.value).toBe("");
1414
+ expect(instance.queryAllOptions()).toHaveLength(2);
1415
+ expect(changed).not.toHaveBeenCalled();
1416
+ });
1417
+ });
1418
+
1419
+ describe("PbDropdown multi-select identity", () => {
1420
+ afterEach(() => {
1421
+ document.body.innerHTML = "";
1422
+ });
1423
+
1424
+ const mount = (options, { asyncSearch = false } = {}) => {
1425
+ const root = document.createElement("div");
1426
+ root.setAttribute("data-pb-dropdown", "true");
1427
+ root.id = "multi-identity";
1428
+ root.dataset.pbDropdownDisabled = "false";
1429
+ root.dataset.pbDropdownMultiSelect = "true";
1430
+ root.dataset.pbDropdownClearable = "true";
1431
+ if (asyncSearch) root.dataset.pbDropdownAsync = "true";
1432
+ root.innerHTML = `
1433
+ <div class="dropdown_wrapper">
1434
+ <input data-dropdown-selected-option name="country[]" style="display: none" />
1435
+ <div class="pb_dropdown_trigger">
1436
+ <input data-dropdown-autocomplete type="text" />
1437
+ <div data-dropdown-pills-wrapper></div>
1438
+ </div>
1439
+ <div class="pb_dropdown_container close" data-dropdown-container="true">
1440
+ <div class="pb_list_kit"></div>
1441
+ </div>
1442
+ </div>
1443
+ `;
1444
+ const list = root.querySelector(".pb_list_kit");
1445
+ options.forEach((option) => {
1446
+ const el = document.createElement("div");
1447
+ el.className = "pb_dropdown_option_list";
1448
+ el.dataset.dropdownOptionLabel = JSON.stringify(option);
1449
+ el.innerHTML = `<div class="pb_body_kit_light">${option.label}</div>`;
1450
+ list.appendChild(el);
1451
+ });
1452
+ document.body.appendChild(root);
1453
+ const instance = new PbDropdown(root);
1454
+ instance.connect();
1455
+ return { root, instance };
1456
+ };
1457
+
1458
+ test("value-only options stay distinct when clicked, filtered, and removed", () => {
1459
+ const { root, instance } = mount([
1460
+ { label: "Canada", value: "ca" },
1461
+ { label: "Mexico", value: "mx" },
1462
+ ]);
1463
+ const rows = () => instance.queryAllOptions();
1464
+ rows()[0].click();
1465
+ expect(rows()[0].style.display).toBe("none");
1466
+ expect(rows()[1].style.display).toBe("");
1467
+ rows()[1].click();
1468
+ expect(Array.from(instance.selectedOptions).map(JSON.parse)).toEqual([
1469
+ { label: "Canada", value: "ca" },
1470
+ { label: "Mexico", value: "mx" },
1471
+ ]);
1472
+ expect(rows()[0].style.display).toBe("none");
1473
+ expect(rows()[1].style.display).toBe("none");
1474
+ expect(Array.from(root.querySelectorAll("input[data-generated]")).map((input) => input.value).sort()).toEqual(["ca", "mx"]);
1475
+
1476
+ root.querySelector(".pb_form_pill_close").click();
1477
+ expect(Array.from(instance.selectedOptions).map(JSON.parse)).toEqual([{ label: "Mexico", value: "mx" }]);
1478
+ expect(rows()[0].style.display).toBe("");
1479
+ expect(rows()[1].style.display).toBe("none");
1480
+ instance.disconnect();
1481
+ });
1482
+
1483
+ test("options with neither id nor value match the full payload", () => {
1484
+ const { instance } = mount([
1485
+ { label: "Red" },
1486
+ { label: "Blue" },
1487
+ ]);
1488
+ const rows = () => instance.queryAllOptions();
1489
+ rows()[0].click();
1490
+ rows()[1].click();
1491
+ expect(instance.selectedOptions.size).toBe(2);
1492
+ expect(rows()[0].style.display).toBe("none");
1493
+ expect(rows()[1].style.display).toBe("none");
1494
+ rows()[0].click();
1495
+ expect(Array.from(instance.selectedOptions).map(JSON.parse)).toEqual([{ label: "Blue" }]);
1496
+ expect(rows()[0].style.display).toBe("");
1497
+ expect(rows()[1].style.display).toBe("none");
1498
+ instance.disconnect();
1499
+ });
1500
+
1501
+ test("async results hide only the selected value-only option", () => {
1502
+ jest.useFakeTimers();
1503
+ const { root, instance } = mount([
1504
+ { label: "Canada", value: "ca" },
1505
+ { label: "Mexico", value: "mx" },
1506
+ ], { asyncSearch: true });
1507
+ const search = jest.fn();
1508
+ root.addEventListener("pb:dropdown:search", search);
1509
+ instance.queryAllOptions()[0].click();
1510
+ const input = root.querySelector("[data-dropdown-autocomplete]");
1511
+ input.value = "mex";
1512
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1513
+ jest.advanceTimersByTime(250);
1514
+ search.mock.calls[0][0].detail.setResults([
1515
+ { label: "Canada", value: "ca" },
1516
+ { label: "Mexico", value: "mx" },
1517
+ ]);
1518
+ const rows = instance.queryAllOptions();
1519
+ expect(rows[0].style.display).toBe("none");
1520
+ expect(rows[1].style.display).toBe("");
1521
+ expect(JSON.parse(rows[0].dataset.dropdownOptionLabel).id).toBe("ca");
1522
+ instance.disconnect();
1523
+ jest.useRealTimers();
1524
+ });
1525
+ });