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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +1 -4
- data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +9 -5
- data/app/pb_kits/playbook/pb_advanced_table/Components/TablePagination.tsx +28 -6
- data/app/pb_kits/playbook/pb_advanced_table/Components/VirtualizedTableView.tsx +6 -1
- data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +26 -9
- data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.tsx +2 -0
- data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +137 -1
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_section_separator.jsx +90 -0
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_section_separator.md +5 -0
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pagination_with_props.md +2 -1
- data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_advanced_table/table_body.rb +4 -4
- data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +8 -3
- data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +19 -6
- data/app/pb_kits/playbook/pb_advanced_table/table_row.rb +1 -1
- data/app/pb_kits/playbook/pb_background/_background.tsx +3 -1
- data/app/pb_kits/playbook/pb_background/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_caption/_caption.tsx +3 -1
- data/app/pb_kits/playbook/pb_caption/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_container/_container.tsx +3 -1
- data/app/pb_kits/playbook/pb_container/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_currency/_currency.tsx +3 -1
- data/app/pb_kits/playbook/pb_currency/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_dashboard_value/_dashboard_value.tsx +3 -1
- data/app/pb_kits/playbook/pb_dashboard_value/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_date_picker/_date_picker.scss +4 -0
- data/app/pb_kits/playbook/pb_date_picker/_date_picker.tsx +51 -49
- data/app/pb_kits/playbook/pb_date_picker/date_picker.html.erb +56 -56
- data/app/pb_kits/playbook/pb_date_picker/sass_partials/_calendar_input_icon.scss +1 -1
- data/app/pb_kits/playbook/pb_detail/_detail.tsx +3 -1
- data/app/pb_kits/playbook/pb_detail/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +16 -0
- data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +190 -29
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_custom_content.html.erb +58 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_custom_content.md +10 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_initial_options.jsx +45 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_initial_options.md +9 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_loading.jsx +43 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_loading.md +9 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_multi_select.jsx +44 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_multi_select.md +5 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_multi_select_rails.html.erb +44 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_multi_select_rails.md +5 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_rich_results.jsx +64 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_rich_results.md +5 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_search.html.erb +43 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_async_search.md +55 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_controlled_selection.jsx +42 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_controlled_selection.md +5 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_external_search.jsx +58 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_external_search.md +9 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_no_options_text.html.erb +9 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_no_options_text.jsx +35 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_no_options_text_rails.md +3 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_no_options_text_react.md +3 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom.md +7 -2
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_custom_rails.md +7 -2
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.html.erb +33 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.jsx +42 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_enable_time.md +24 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_highlight.jsx +101 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_highlight.md +3 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/_playground.json +174 -3
- data/app/pb_kits/playbook/pb_dropdown/docs/_playground.overrides.json +154 -2
- data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +14 -0
- data/app/pb_kits/playbook/pb_dropdown/docs/index.js +9 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +4 -1
- data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +14 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +1344 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown_container.html.erb +1 -1
- data/app/pb_kits/playbook/pb_dropdown/dropdown_container.rb +1 -0
- data/app/pb_kits/playbook/pb_dropdown/dropdown_index.test.js +1153 -0
- data/app/pb_kits/playbook/pb_dropdown/hooks/useAsyncOptions.ts +179 -0
- data/app/pb_kits/playbook/pb_dropdown/hooks/useHandleOnKeydown.tsx +21 -19
- data/app/pb_kits/playbook/pb_dropdown/index.js +518 -79
- data/app/pb_kits/playbook/pb_dropdown/keyboard_accessibility.js +5 -3
- data/app/pb_kits/playbook/pb_dropdown/kit.schema.json +78 -1
- data/app/pb_kits/playbook/pb_dropdown/quickpick/index.ts +60 -18
- data/app/pb_kits/playbook/pb_dropdown/quickpick_helper.rb +92 -3
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownContainer.tsx +17 -3
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownOption.tsx +21 -7
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownTrigger.tsx +5 -2
- data/app/pb_kits/playbook/pb_dropdown/subcomponents/MultiSelectTriggerDisplay.tsx +9 -3
- data/app/pb_kits/playbook/pb_dropdown/utilities/index.ts +8 -1
- data/app/pb_kits/playbook/pb_dropdown/utilities/quickpickDateSync.ts +137 -0
- data/app/pb_kits/playbook/pb_filter/Filter/InteractiveFilter.tsx +1 -1
- data/app/pb_kits/playbook/pb_form/docs/_description_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_default_values.jsx +898 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_default_values_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_defaults.jsx +879 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_defaults_react.md +29 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_loading.jsx +75 -0
- data/app/pb_kits/playbook/pb_form/docs/{_form_loading.md → _form_loading_rails.md} +1 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_loading_react.md +1 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_model_values.jsx +147 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_model_values_react.md +16 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_multi_column.jsx +164 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_multi_column_react.md +3 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_validation.html.erb +27 -4
- data/app/pb_kits/playbook/pb_form/docs/_form_validation.jsx +1057 -0
- data/app/pb_kits/playbook/pb_form/docs/{_form_validation.md → _form_validation_rails.md} +4 -4
- data/app/pb_kits/playbook/pb_form/docs/_form_validation_react.md +26 -0
- data/app/pb_kits/playbook/pb_form/docs/example.yml +7 -0
- data/app/pb_kits/playbook/pb_form/docs/snippets/validation_checkbox.txt +6 -1
- data/app/pb_kits/playbook/pb_form/docs/snippets/validation_dropdown.txt +6 -1
- data/app/pb_kits/playbook/pb_form/docs/snippets/validation_multi_level_select.txt +1 -0
- data/app/pb_kits/playbook/pb_form/docs/snippets/validation_multi_select.txt +1 -0
- data/app/pb_kits/playbook/pb_form/docs/snippets/validation_phone_number.txt +1 -0
- data/app/pb_kits/playbook/pb_form/docs/snippets/validation_star_rating.txt +6 -1
- data/app/pb_kits/playbook/pb_form/docs/snippets/validation_textarea.txt +5 -1
- data/app/pb_kits/playbook/pb_form/docs/snippets/validation_time_zone_select.txt +1 -0
- data/app/pb_kits/playbook/pb_form/pb_form_validation.js +52 -34
- data/app/pb_kits/playbook/pb_full_screen/_full_screen.scss +6 -1
- data/app/pb_kits/playbook/pb_full_screen/full_screen.test.jsx +19 -7
- data/app/pb_kits/playbook/pb_highlight/_highlight.tsx +3 -1
- data/app/pb_kits/playbook/pb_highlight/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_icon_stat_value/_icon_stat_value.tsx +3 -1
- data/app/pb_kits/playbook/pb_icon_stat_value/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_icon_value/_icon_value.tsx +3 -1
- data/app/pb_kits/playbook/pb_icon_value/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_label_value/_label_value.tsx +3 -1
- data/app/pb_kits/playbook/pb_label_value/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_legend/_legend.tsx +3 -1
- data/app/pb_kits/playbook/pb_legend/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_lightbox/Header/_lightbox_header.tsx +1 -1
- data/app/pb_kits/playbook/pb_lightbox/lightbox.test.jsx +1 -1
- data/app/pb_kits/playbook/pb_map/docs/_map_default.jsx +1 -1
- data/app/pb_kits/playbook/pb_map/docs/_map_default.md +3 -3
- data/app/pb_kits/playbook/pb_map/docs/_map_with_custom_button.jsx +1 -1
- data/app/pb_kits/playbook/pb_map/docs/_map_with_plugin.jsx +1 -1
- data/app/pb_kits/playbook/pb_map/docs/_playground.json +3 -3
- data/app/pb_kits/playbook/pb_map/docs/_playground.overrides.json +3 -3
- data/app/pb_kits/playbook/pb_message/_message.tsx +3 -1
- data/app/pb_kits/playbook/pb_message/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/index.js +19 -4
- data/app/pb_kits/playbook/pb_multi_level_select/kit.schema.json +7 -0
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.html.erb +4 -1
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.rb +7 -3
- data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +3 -1
- data/app/pb_kits/playbook/pb_multiple_users/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_multiple_users_stacked/_multiple_users_stacked.tsx +4 -2
- data/app/pb_kits/playbook/pb_multiple_users_stacked/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +10 -7
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_playground.json +1 -0
- data/app/pb_kits/playbook/pb_phone_number_input/index.ts +7 -2
- data/app/pb_kits/playbook/pb_phone_number_input/kit.schema.json +8 -0
- data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.rb +5 -1
- data/app/pb_kits/playbook/pb_phone_number_input/phone_number_input.test.js +12 -0
- data/app/pb_kits/playbook/pb_progress_step/_progress_step_item.tsx +3 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/MoreExtensionsDropdown.tsx +10 -3
- data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/Toolbar.tsx +16 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.json +2 -3
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.overrides.json +2 -3
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_attributes.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_default.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_default.md +2 -3
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_focus.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_height.jsx +3 -13
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_inline.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_label.jsx +2 -3
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.jsx +1 -2
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_min_height.jsx +3 -13
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_preview.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_simple.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_sticky.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_templates.jsx +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_more_extensions.jsx +3 -8
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_more_extensions.md +1 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_default.md +2 -2
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.test.js +2 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_advanced.test.js +7 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_rails.js +15 -4
- data/app/pb_kits/playbook/pb_select/_select.scss +2 -2
- data/app/pb_kits/playbook/pb_star_rating/index.js +8 -4
- data/app/pb_kits/playbook/pb_star_rating/kit.schema.json +7 -0
- data/app/pb_kits/playbook/pb_star_rating/star_rating.rb +6 -1
- data/app/pb_kits/playbook/pb_stat_value/_stat_value.tsx +3 -1
- data/app/pb_kits/playbook/pb_stat_value/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_table/_table.tsx +70 -197
- data/app/pb_kits/playbook/pb_table/index.ts +53 -122
- data/app/pb_kits/playbook/pb_table/styles/_sticky_columns.scss +26 -3
- data/app/pb_kits/playbook/pb_table/styles/_vertical_border.scss +27 -8
- data/app/pb_kits/playbook/pb_table/utilities/stickyColumns.test.ts +266 -0
- data/app/pb_kits/playbook/pb_table/utilities/stickyColumns.ts +145 -0
- data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +1 -1
- data/app/pb_kits/playbook/pb_text_input/add_on.html.erb +2 -2
- data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -1
- data/app/pb_kits/playbook/pb_title_count/_title_count.tsx +3 -1
- data/app/pb_kits/playbook/pb_title_count/kit.schema.json +1 -0
- data/app/pb_kits/playbook/pb_title_detail/_title_detail.tsx +3 -1
- data/app/pb_kits/playbook/pb_title_detail/kit.schema.json +1 -0
- data/app/pb_kits/playbook/utilities/test/globalProps/onClick.test.js +327 -0
- data/dist/ai/all-schemas.json +277 -55
- data/dist/ai/forms.json +26 -20
- data/dist/ai/index.json +2 -2
- data/dist/ai/kits/background.schema.json +1 -0
- data/dist/ai/kits/button.schema.json +1 -1
- data/dist/ai/kits/caption.schema.json +1 -0
- data/dist/ai/kits/checkbox.schema.json +7 -5
- data/dist/ai/kits/container.schema.json +1 -0
- data/dist/ai/kits/currency.schema.json +1 -0
- data/dist/ai/kits/dashboard_value.schema.json +1 -0
- data/dist/ai/kits/date_picker.schema.json +4 -3
- data/dist/ai/kits/detail.schema.json +1 -0
- data/dist/ai/kits/dropdown.schema.json +160 -10
- data/dist/ai/kits/highlight.schema.json +1 -0
- data/dist/ai/kits/icon_stat_value.schema.json +1 -0
- data/dist/ai/kits/icon_value.schema.json +1 -0
- data/dist/ai/kits/label_value.schema.json +1 -0
- data/dist/ai/kits/legend.schema.json +1 -0
- data/dist/ai/kits/message.schema.json +1 -0
- data/dist/ai/kits/multi_level_select.schema.json +15 -6
- data/dist/ai/kits/multiple_users.schema.json +1 -0
- data/dist/ai/kits/multiple_users_stacked.schema.json +1 -0
- data/dist/ai/kits/phone_number_input.schema.json +18 -7
- data/dist/ai/kits/select.schema.json +25 -6
- data/dist/ai/kits/star_rating.schema.json +14 -5
- data/dist/ai/kits/stat_value.schema.json +1 -0
- data/dist/ai/kits/text_input.schema.json +4 -3
- data/dist/ai/kits/textarea.schema.json +4 -3
- data/dist/ai/kits/time_picker.schema.json +4 -3
- data/dist/ai/kits/title_count.schema.json +1 -0
- data/dist/ai/kits/title_detail.schema.json +1 -0
- data/dist/ai/kits/typeahead.schema.json +4 -3
- data/dist/ai/playgrounds/dropdown.json +1 -1
- data/dist/ai/playgrounds/index.json +1 -1
- data/dist/ai/playgrounds/map.json +1 -1
- data/dist/ai/playgrounds/rich_text_editor.json +1 -1
- data/dist/chunks/_pagination-BStnaG2D.js +1 -0
- data/dist/chunks/{_pb_line_graph-CUJ7NJIr.js → _pb_line_graph-DAoYQIoh.js} +1 -1
- data/dist/chunks/_tooltip-WptzLsCF.js +5 -0
- data/dist/chunks/_typeahead-DNfFwTns.js +1 -0
- data/dist/chunks/{globalProps-DDcpyjvz.js → globalProps-wXibGgWr.js} +1 -1
- data/dist/chunks/lib-BDUQ3wt0.js +29 -0
- data/dist/chunks/quickpickDateSync-DLDx4zce.js +1 -0
- data/dist/chunks/{time_picker_helper-B_FN8E9Y.js → time_picker_helper-Dc38LjqW.js} +1 -1
- data/dist/chunks/vendor.js +2 -2
- data/dist/menu.yml +1 -1
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/forms/builder/checkbox_field.rb +1 -0
- data/lib/playbook/forms/builder/collection_select_field.rb +5 -1
- data/lib/playbook/forms/builder/select_field.rb +5 -1
- data/lib/playbook/version.rb +2 -2
- metadata +63 -16
- data/dist/chunks/_detail-CDUjsRrc.js +0 -5
- data/dist/chunks/_table-C9KHGQhb.js +0 -1
- data/dist/chunks/_typeahead-CfxZLhd3.js +0 -1
- data/dist/chunks/lib-D57mygNT.js +0 -29
- /data/app/pb_kits/playbook/pb_form/docs/{_description.md → _description_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_default_values.md → _form_default_values_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_defaults.md → _form_defaults_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_model_values.md → _form_model_values_rails.md} +0 -0
- /data/app/pb_kits/playbook/pb_form/docs/{_form_multi_column.md → _form_multi_column_rails.md} +0 -0
|
@@ -1199,3 +1199,1347 @@ test('autocomplete selection still fires onSelect', () => {
|
|
|
1199
1199
|
|
|
1200
1200
|
expect(onSelect).toHaveBeenCalledWith(options[1])
|
|
1201
1201
|
})
|
|
1202
|
+
|
|
1203
|
+
describe('async Dropdown', () => {
|
|
1204
|
+
beforeEach(() => jest.useFakeTimers());
|
|
1205
|
+
afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); });
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
test('ignores stale callbacks and cancels pending search on Escape', () => {
|
|
1209
|
+
const loadOptions = jest.fn();
|
|
1210
|
+
render(<Dropdown async
|
|
1211
|
+
autocomplete
|
|
1212
|
+
loadOptions={loadOptions}
|
|
1213
|
+
/>);
|
|
1214
|
+
const input = screen.getByRole('textbox');
|
|
1215
|
+
fireEvent.change(input, { target: { value: 'first' } });
|
|
1216
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1217
|
+
fireEvent.change(input, { target: { value: 'second' } });
|
|
1218
|
+
act(() => loadOptions.mock.calls[0][1]([{ id: '1', label: 'Stale' }]));
|
|
1219
|
+
expect(screen.queryByText('Stale')).not.toBeInTheDocument();
|
|
1220
|
+
fireEvent.keyDown(input, { key: 'Escape', code: 'Escape' });
|
|
1221
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1222
|
+
expect(loadOptions).toHaveBeenCalledTimes(1);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
test('supports promise results and rejected requests', async () => {
|
|
1226
|
+
const loadOptions = jest.fn().mockResolvedValueOnce([]).mockRejectedValueOnce(new Error('Failed'));
|
|
1227
|
+
render(<Dropdown async
|
|
1228
|
+
autocomplete
|
|
1229
|
+
loadOptions={loadOptions}
|
|
1230
|
+
/>);
|
|
1231
|
+
const input = screen.getByRole('textbox');
|
|
1232
|
+
fireEvent.change(input, { target: { value: 'empty' } });
|
|
1233
|
+
await act(async () => { jest.advanceTimersByTime(250); });
|
|
1234
|
+
expect(screen.getByRole('status')).toHaveTextContent('No results found');
|
|
1235
|
+
fireEvent.change(input, { target: { value: 'error' } });
|
|
1236
|
+
await act(async () => { jest.advanceTimersByTime(250); });
|
|
1237
|
+
expect(screen.getByRole('status')).toHaveTextContent('Unable to load options');
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
describe('async Dropdown lifecycle', () => {
|
|
1242
|
+
beforeEach(() => jest.useFakeTimers());
|
|
1243
|
+
afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); });
|
|
1244
|
+
|
|
1245
|
+
test.each(['Tab', 'Escape'])('dismissal with %s invalidates in-flight responses', (key) => {
|
|
1246
|
+
const loadOptions = jest.fn();
|
|
1247
|
+
render(<Dropdown async
|
|
1248
|
+
autocomplete
|
|
1249
|
+
loadOptions={loadOptions}
|
|
1250
|
+
/>);
|
|
1251
|
+
const input = screen.getByRole('textbox');
|
|
1252
|
+
fireEvent.change(input, { target: { value: 'query' } });
|
|
1253
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1254
|
+
fireEvent.keyDown(input, { key });
|
|
1255
|
+
act(() => loadOptions.mock.calls[0][1]([{ id: 'late', label: 'Late result' }]));
|
|
1256
|
+
expect(screen.queryByText('Late result')).not.toBeInTheDocument();
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
test('times out callback loaders and ignores their late results', () => {
|
|
1261
|
+
const loadOptions = jest.fn();
|
|
1262
|
+
render(<Dropdown async
|
|
1263
|
+
autocomplete
|
|
1264
|
+
loadOptions={loadOptions}
|
|
1265
|
+
/>);
|
|
1266
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'a' } });
|
|
1267
|
+
act(() => jest.advanceTimersByTime(15000));
|
|
1268
|
+
expect(screen.getByRole('status')).toHaveTextContent('Unable to load options');
|
|
1269
|
+
act(() => loadOptions.mock.calls[0][1]([{ id: 'late', label: 'Late result' }]));
|
|
1270
|
+
expect(screen.queryByText('Late result')).not.toBeInTheDocument();
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test('unmount cancels debounced work', () => {
|
|
1274
|
+
const loadOptions = jest.fn();
|
|
1275
|
+
const { unmount } = render(<Dropdown async
|
|
1276
|
+
autocomplete
|
|
1277
|
+
loadOptions={loadOptions}
|
|
1278
|
+
/>);
|
|
1279
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } });
|
|
1280
|
+
unmount();
|
|
1281
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1282
|
+
expect(loadOptions).not.toHaveBeenCalled();
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
describe('async Dropdown cancellation', () => {
|
|
1287
|
+
beforeEach(() => jest.useFakeTimers());
|
|
1288
|
+
afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); });
|
|
1289
|
+
|
|
1290
|
+
test.each(['Escape', 'outside', 'disable'])('reopening retries an interrupted search after %s', (dismissal) => {
|
|
1291
|
+
const loadOptions = jest.fn();
|
|
1292
|
+
const example = (disabled = false) => (
|
|
1293
|
+
<Dropdown async
|
|
1294
|
+
autocomplete
|
|
1295
|
+
disabled={disabled}
|
|
1296
|
+
loadOptions={loadOptions}
|
|
1297
|
+
/>
|
|
1298
|
+
);
|
|
1299
|
+
const { rerender } = render(example());
|
|
1300
|
+
const input = screen.getByRole('textbox');
|
|
1301
|
+
fireEvent.change(input, { target: { value: 'Canada' } });
|
|
1302
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1303
|
+
const staleCallback = loadOptions.mock.calls[0][1];
|
|
1304
|
+
if (dismissal === 'disable') rerender(example(true));
|
|
1305
|
+
else if (dismissal === 'outside') fireEvent.click(document.body);
|
|
1306
|
+
else fireEvent.keyDown(input, { key: 'Escape' });
|
|
1307
|
+
act(() => staleCallback([{ id: 'stale', label: 'Stale result' }]));
|
|
1308
|
+
if (dismissal === 'disable') rerender(example());
|
|
1309
|
+
expect(input).toHaveValue('Canada');
|
|
1310
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
1311
|
+
expect(screen.getByRole('status')).toHaveTextContent('Loading');
|
|
1312
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1313
|
+
expect(loadOptions).toHaveBeenCalledTimes(2);
|
|
1314
|
+
expect(loadOptions.mock.calls[1][0]).toBe('Canada');
|
|
1315
|
+
act(() => loadOptions.mock.calls[1][1]([options[1]]));
|
|
1316
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1317
|
+
expect(screen.queryByText('Stale result')).not.toBeInTheDocument();
|
|
1318
|
+
fireEvent.keyDown(input, { key: 'Escape' });
|
|
1319
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
1320
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1321
|
+
expect(loadOptions).toHaveBeenCalledTimes(2);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
test.each(['Escape', 'outside', 'disable'])('cancelled search restores settled options and retries the query on reopen after %s', (dismissal) => {
|
|
1325
|
+
const loadOptions = jest.fn();
|
|
1326
|
+
const example = (disabled = false) => (
|
|
1327
|
+
<Dropdown async
|
|
1328
|
+
autocomplete
|
|
1329
|
+
defaultOptions={options}
|
|
1330
|
+
disabled={disabled}
|
|
1331
|
+
loadOptions={loadOptions}
|
|
1332
|
+
/>
|
|
1333
|
+
);
|
|
1334
|
+
const { rerender } = render(example());
|
|
1335
|
+
const input = screen.getByRole('textbox');
|
|
1336
|
+
fireEvent.change(input, { target: { value: 'Can' } });
|
|
1337
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1338
|
+
act(() => loadOptions.mock.calls[0][1]([options[1]]));
|
|
1339
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1340
|
+
expect(screen.queryByText('United States')).not.toBeInTheDocument();
|
|
1341
|
+
|
|
1342
|
+
fireEvent.change(input, { target: { value: 'Pak' } });
|
|
1343
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1344
|
+
expect(screen.queryByText('Canada')).not.toBeInTheDocument();
|
|
1345
|
+
expect(screen.getByRole('status')).toHaveTextContent('Loading');
|
|
1346
|
+
|
|
1347
|
+
if (dismissal === 'disable') rerender(example(true));
|
|
1348
|
+
else if (dismissal === 'outside') fireEvent.click(document.body);
|
|
1349
|
+
else fireEvent.keyDown(input, { key: 'Escape' });
|
|
1350
|
+
|
|
1351
|
+
act(() => loadOptions.mock.calls[1][1]([{ id: 'late', label: 'Late result' }]));
|
|
1352
|
+
expect(screen.queryByText('Late result')).not.toBeInTheDocument();
|
|
1353
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1354
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
1355
|
+
expect(input).toHaveValue('Pak');
|
|
1356
|
+
|
|
1357
|
+
if (dismissal === 'disable') rerender(example());
|
|
1358
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
1359
|
+
expect(screen.getByRole('status')).toHaveTextContent('Loading');
|
|
1360
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1361
|
+
expect(loadOptions).toHaveBeenCalledTimes(3);
|
|
1362
|
+
expect(loadOptions.mock.calls[2][0]).toBe('Pak');
|
|
1363
|
+
act(() => loadOptions.mock.calls[2][1]([options[2]]));
|
|
1364
|
+
expect(screen.getByText('Pakistan')).toBeInTheDocument();
|
|
1365
|
+
expect(screen.queryByText('Late result')).not.toBeInTheDocument();
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
test.each(['error', 'empty'])('closing a settled %s search keeps that outcome', (outcome) => {
|
|
1369
|
+
const loadOptions = jest.fn();
|
|
1370
|
+
render(<Dropdown async
|
|
1371
|
+
autocomplete
|
|
1372
|
+
defaultOptions={options}
|
|
1373
|
+
loadOptions={loadOptions}
|
|
1374
|
+
/>);
|
|
1375
|
+
const input = screen.getByRole('textbox');
|
|
1376
|
+
fireEvent.change(input, { target: { value: 'Can' } });
|
|
1377
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1378
|
+
act(() => loadOptions.mock.calls[0][1](outcome === 'empty' ? [] : null));
|
|
1379
|
+
expect(screen.getByRole('status')).toHaveTextContent(outcome === 'empty' ? 'No results found' : 'Unable to load options');
|
|
1380
|
+
expect(screen.queryByText('Canada')).not.toBeInTheDocument();
|
|
1381
|
+
|
|
1382
|
+
fireEvent.keyDown(input, { key: 'Escape' });
|
|
1383
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
1384
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1385
|
+
expect(loadOptions).toHaveBeenCalledTimes(1);
|
|
1386
|
+
expect(screen.getByRole('status')).toHaveTextContent(outcome === 'empty' ? 'No results found' : 'Unable to load options');
|
|
1387
|
+
expect(screen.queryByText('Canada')).not.toBeInTheDocument();
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
test('clearing a cancelled search prevents retry on reopen', () => {
|
|
1391
|
+
const loadOptions = jest.fn();
|
|
1392
|
+
const ref = React.createRef();
|
|
1393
|
+
render(<Dropdown async
|
|
1394
|
+
autocomplete
|
|
1395
|
+
loadOptions={loadOptions}
|
|
1396
|
+
ref={ref}
|
|
1397
|
+
/>);
|
|
1398
|
+
const input = screen.getByRole('textbox');
|
|
1399
|
+
fireEvent.change(input, { target: { value: 'Canada' } });
|
|
1400
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1401
|
+
fireEvent.keyDown(input, { key: 'Escape' });
|
|
1402
|
+
act(() => ref.current.clearSelected());
|
|
1403
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
1404
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1405
|
+
expect(loadOptions).toHaveBeenCalledTimes(1);
|
|
1406
|
+
expect(input).toHaveValue('');
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
test('outside click cancels a pending debounce', () => {
|
|
1411
|
+
const loadOptions = jest.fn();
|
|
1412
|
+
render(<Dropdown async
|
|
1413
|
+
autocomplete
|
|
1414
|
+
loadOptions={loadOptions}
|
|
1415
|
+
/>);
|
|
1416
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } });
|
|
1417
|
+
fireEvent.click(document.body);
|
|
1418
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1419
|
+
expect(loadOptions).not.toHaveBeenCalled();
|
|
1420
|
+
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowDown' });
|
|
1421
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1422
|
+
expect(loadOptions).toHaveBeenCalledTimes(1);
|
|
1423
|
+
expect(loadOptions.mock.calls[0][0]).toBe('query');
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test('disabling invalidates an in-flight response', () => {
|
|
1427
|
+
const loadOptions = jest.fn();
|
|
1428
|
+
const { rerender } = render(<Dropdown async
|
|
1429
|
+
autocomplete
|
|
1430
|
+
loadOptions={loadOptions}
|
|
1431
|
+
/>);
|
|
1432
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } });
|
|
1433
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1434
|
+
rerender(<Dropdown async
|
|
1435
|
+
autocomplete
|
|
1436
|
+
disabled
|
|
1437
|
+
loadOptions={loadOptions}
|
|
1438
|
+
/>);
|
|
1439
|
+
act(() => loadOptions.mock.calls[0][1]([{ id: 'late', label: 'Late result' }]));
|
|
1440
|
+
expect(screen.queryByText('Late result')).not.toBeInTheDocument();
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
test('re-enabling a preloaded dropdown retries the interrupted query instead of preloading', () => {
|
|
1444
|
+
const loadOptions = jest.fn();
|
|
1445
|
+
const example = (disabled = false) => (
|
|
1446
|
+
<Dropdown async
|
|
1447
|
+
autocomplete
|
|
1448
|
+
defaultOptions
|
|
1449
|
+
disabled={disabled}
|
|
1450
|
+
loadOptions={loadOptions}
|
|
1451
|
+
/>
|
|
1452
|
+
);
|
|
1453
|
+
const { rerender } = render(example());
|
|
1454
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1455
|
+
act(() => loadOptions.mock.calls[0][1](options));
|
|
1456
|
+
const input = screen.getByRole('textbox');
|
|
1457
|
+
fireEvent.change(input, { target: { value: 'Canada' } });
|
|
1458
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1459
|
+
rerender(example(true));
|
|
1460
|
+
rerender(example());
|
|
1461
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1462
|
+
expect(loadOptions).toHaveBeenCalledTimes(2);
|
|
1463
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
1464
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1465
|
+
expect(loadOptions).toHaveBeenCalledTimes(3);
|
|
1466
|
+
expect(loadOptions.mock.calls[2][0]).toBe('Canada');
|
|
1467
|
+
act(() => loadOptions.mock.calls[2][1]([options[1]]));
|
|
1468
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
test('clear does not request an empty query', () => {
|
|
1472
|
+
const loadOptions = jest.fn();
|
|
1473
|
+
const ref = React.createRef();
|
|
1474
|
+
render(<Dropdown async
|
|
1475
|
+
autocomplete
|
|
1476
|
+
loadOptions={loadOptions}
|
|
1477
|
+
ref={ref}
|
|
1478
|
+
/>);
|
|
1479
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } });
|
|
1480
|
+
act(() => ref.current.clearSelected());
|
|
1481
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1482
|
+
expect(loadOptions).not.toHaveBeenCalled();
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
describe('Dropdown rich option rendering', () => {
|
|
1487
|
+
test.each([false, true])('renders remote content and preserves selection data (searchbar: %s)', async (searchbar) => {
|
|
1488
|
+
const option = { id: 'user', value: 'user', label: 'Ada', company: { title: 'Engineer' } };
|
|
1489
|
+
const onSelect = jest.fn();
|
|
1490
|
+
const loadOptions = jest.fn().mockResolvedValue([option]);
|
|
1491
|
+
render(
|
|
1492
|
+
<Dropdown async
|
|
1493
|
+
autocomplete={!searchbar}
|
|
1494
|
+
loadOptions={loadOptions}
|
|
1495
|
+
onSelect={onSelect}
|
|
1496
|
+
renderOption={(user) => <span>{user.company.title}</span>}
|
|
1497
|
+
>
|
|
1498
|
+
{searchbar ? [<Dropdown.Trigger key="trigger" />, <Dropdown.Container key="container"
|
|
1499
|
+
searchbar
|
|
1500
|
+
/>] : undefined}
|
|
1501
|
+
</Dropdown>
|
|
1502
|
+
);
|
|
1503
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'ada' } });
|
|
1504
|
+
const result = await screen.findByText('Engineer');
|
|
1505
|
+
if (searchbar) {
|
|
1506
|
+
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowDown' });
|
|
1507
|
+
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' });
|
|
1508
|
+
} else {
|
|
1509
|
+
fireEvent.click(result);
|
|
1510
|
+
}
|
|
1511
|
+
expect(onSelect).toHaveBeenCalledWith(option);
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
test('explicit children override the renderer and null falls back to the label', () => {
|
|
1515
|
+
const renderOption = jest.fn(() => null);
|
|
1516
|
+
render(
|
|
1517
|
+
<Dropdown options={options}
|
|
1518
|
+
renderOption={renderOption}
|
|
1519
|
+
>
|
|
1520
|
+
<Dropdown.Option option={options[0]}><span>{'Explicit content'}</span></Dropdown.Option>
|
|
1521
|
+
<Dropdown.Option option={options[1]} />
|
|
1522
|
+
</Dropdown>
|
|
1523
|
+
);
|
|
1524
|
+
expect(screen.getByText('Explicit content')).toBeInTheDocument();
|
|
1525
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1526
|
+
expect(renderOption).not.toHaveBeenCalledWith(options[0]);
|
|
1527
|
+
expect(renderOption).toHaveBeenCalledWith(options[1]);
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
describe('Dropdown new-prop compatibility gates', () => {
|
|
1532
|
+
test.each([undefined, false])('keeps local filtering and callbacks when async is %s', (async) => {
|
|
1533
|
+
const loadOptions = jest.fn();
|
|
1534
|
+
const onChange = jest.fn();
|
|
1535
|
+
const onSelect = jest.fn();
|
|
1536
|
+
const { container } = render(
|
|
1537
|
+
<Dropdown async={async}
|
|
1538
|
+
autocomplete
|
|
1539
|
+
loadOptions={loadOptions}
|
|
1540
|
+
name="country"
|
|
1541
|
+
onChange={onChange}
|
|
1542
|
+
onSelect={onSelect}
|
|
1543
|
+
options={options}
|
|
1544
|
+
/>
|
|
1545
|
+
);
|
|
1546
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Can' } });
|
|
1547
|
+
expect(screen.queryByText('Pakistan')).not.toBeInTheDocument();
|
|
1548
|
+
fireEvent.click(screen.getByText('Canada'));
|
|
1549
|
+
expect(loadOptions).not.toHaveBeenCalled();
|
|
1550
|
+
expect(onSelect).toHaveBeenCalledWith(options[1]);
|
|
1551
|
+
expect(onChange).toHaveBeenCalledWith({ target: { name: 'country', value: options[1] } });
|
|
1552
|
+
expect(container.querySelector('[aria-busy]')).toBeNull();
|
|
1553
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
test('removing async mode restores local results without an async status', () => {
|
|
1557
|
+
const loadOptions = jest.fn();
|
|
1558
|
+
const { rerender } = render(
|
|
1559
|
+
<Dropdown async
|
|
1560
|
+
autocomplete
|
|
1561
|
+
loadOptions={loadOptions}
|
|
1562
|
+
/>
|
|
1563
|
+
);
|
|
1564
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Can' } });
|
|
1565
|
+
expect(screen.getByRole('status')).toHaveTextContent('Loading');
|
|
1566
|
+
rerender(<Dropdown autocomplete
|
|
1567
|
+
options={options}
|
|
1568
|
+
/>);
|
|
1569
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
1570
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1571
|
+
});
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
describe('Dropdown controlled selection and identity', () => {
|
|
1575
|
+
test.each([false, true])('controlled single-select keeps all options after selection (searchbar: %s)', (searchbar) => {
|
|
1576
|
+
const Example = () => {
|
|
1577
|
+
const [selected, setSelected] = useState(options[0]);
|
|
1578
|
+
return (
|
|
1579
|
+
<Dropdown onSelect={setSelected}
|
|
1580
|
+
options={options}
|
|
1581
|
+
value={selected}
|
|
1582
|
+
>
|
|
1583
|
+
<Dropdown.Trigger />
|
|
1584
|
+
<Dropdown.Container searchbar={searchbar}>
|
|
1585
|
+
{options.map((option) => (
|
|
1586
|
+
<Dropdown.Option key={option.id}
|
|
1587
|
+
option={option}
|
|
1588
|
+
/>
|
|
1589
|
+
))}
|
|
1590
|
+
</Dropdown.Container>
|
|
1591
|
+
</Dropdown>
|
|
1592
|
+
);
|
|
1593
|
+
};
|
|
1594
|
+
const { container } = render(<Example />);
|
|
1595
|
+
fireEvent.click(screen.getByText('Canada'));
|
|
1596
|
+
expect(Array.from(container.querySelectorAll('[class*="pb_dropdown_option_"]')).map((option) => option.textContent)).toEqual(options.map((option) => option.label));
|
|
1597
|
+
if (searchbar) expect(screen.getByRole('textbox')).toHaveValue('');
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
test.each([
|
|
1601
|
+
[false, 'icon'], [true, 'icon'],
|
|
1602
|
+
[false, 'ref'], [true, 'ref'],
|
|
1603
|
+
])('controlled single-select clears its search (searchbar: %s, clear: %s)', (searchbar, method) => {
|
|
1604
|
+
const ref = React.createRef();
|
|
1605
|
+
const onSelect = jest.fn();
|
|
1606
|
+
const Example = () => {
|
|
1607
|
+
const [selected, setSelected] = useState(options[0]);
|
|
1608
|
+
return (
|
|
1609
|
+
<Dropdown onSelect={(option) => { onSelect(option); setSelected(option) }}
|
|
1610
|
+
options={options}
|
|
1611
|
+
ref={ref}
|
|
1612
|
+
value={selected}
|
|
1613
|
+
>
|
|
1614
|
+
<Dropdown.Trigger />
|
|
1615
|
+
<Dropdown.Container searchbar={searchbar}>
|
|
1616
|
+
{options.map((option) => (
|
|
1617
|
+
<Dropdown.Option key={option.id}
|
|
1618
|
+
option={option}
|
|
1619
|
+
/>
|
|
1620
|
+
))}
|
|
1621
|
+
</Dropdown.Container>
|
|
1622
|
+
</Dropdown>
|
|
1623
|
+
);
|
|
1624
|
+
};
|
|
1625
|
+
const { container } = render(<Example />);
|
|
1626
|
+
if (searchbar) fireEvent.change(screen.getByRole('textbox'), { target: { value: 'United' } });
|
|
1627
|
+
if (method === 'ref') act(() => ref.current.clearSelected());
|
|
1628
|
+
else fireEvent.click(container.querySelector('[aria-label="times icon"]').closest('div'));
|
|
1629
|
+
expect(onSelect).toHaveBeenLastCalledWith(null);
|
|
1630
|
+
expect(container.querySelectorAll('[class*="pb_dropdown_option_"]')).toHaveLength(options.length);
|
|
1631
|
+
if (searchbar) expect(screen.getByRole('textbox')).toHaveValue('');
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
test('controlled selection waits for the parent and retains full data', () => {
|
|
1635
|
+
const onSelect = jest.fn();
|
|
1636
|
+
const ref = React.createRef();
|
|
1637
|
+
const { rerender } = render(
|
|
1638
|
+
<Dropdown autocomplete
|
|
1639
|
+
onSelect={onSelect}
|
|
1640
|
+
options={options}
|
|
1641
|
+
ref={ref}
|
|
1642
|
+
value={options[0]}
|
|
1643
|
+
/>
|
|
1644
|
+
);
|
|
1645
|
+
fireEvent.click(screen.getByText('Canada'));
|
|
1646
|
+
expect(onSelect).toHaveBeenCalledWith(options[1]);
|
|
1647
|
+
expect(screen.getByRole('textbox')).toHaveValue('United States');
|
|
1648
|
+
rerender(<Dropdown autocomplete
|
|
1649
|
+
onSelect={onSelect}
|
|
1650
|
+
options={[]}
|
|
1651
|
+
ref={ref}
|
|
1652
|
+
value={options[1]}
|
|
1653
|
+
/>);
|
|
1654
|
+
expect(screen.getByRole('textbox')).toHaveValue('Canada');
|
|
1655
|
+
act(() => ref.current.clearSelected());
|
|
1656
|
+
expect(onSelect).toHaveBeenLastCalledWith(null);
|
|
1657
|
+
expect(screen.getByRole('textbox')).toHaveValue('Canada');
|
|
1658
|
+
rerender(<Dropdown autocomplete
|
|
1659
|
+
onSelect={onSelect}
|
|
1660
|
+
options={[]}
|
|
1661
|
+
value={null}
|
|
1662
|
+
/>);
|
|
1663
|
+
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
1664
|
+
expect(onSelect).toHaveBeenCalledTimes(2);
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
test('controlled multi-select emits once without updating until the parent accepts', () => {
|
|
1668
|
+
const onSelect = jest.fn();
|
|
1669
|
+
const { container } = render(
|
|
1670
|
+
<React.StrictMode>
|
|
1671
|
+
<Dropdown multiSelect
|
|
1672
|
+
onSelect={onSelect}
|
|
1673
|
+
options={options}
|
|
1674
|
+
value={[]}
|
|
1675
|
+
/>
|
|
1676
|
+
</React.StrictMode>
|
|
1677
|
+
);
|
|
1678
|
+
fireEvent.click(screen.getByText('Canada'));
|
|
1679
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
1680
|
+
expect(onSelect).toHaveBeenCalledWith([options[1]]);
|
|
1681
|
+
expect(container.querySelectorAll('.pb_dropdown_option_list')).toHaveLength(3);
|
|
1682
|
+
});
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
describe('Dropdown input notifications', () => {
|
|
1686
|
+
test('reports typing and clears single selection only when the input becomes empty', () => {
|
|
1687
|
+
const onInputChange = jest.fn();
|
|
1688
|
+
const onSelect = jest.fn();
|
|
1689
|
+
render(<Dropdown autocomplete
|
|
1690
|
+
defaultValue={options[0]}
|
|
1691
|
+
onInputChange={onInputChange}
|
|
1692
|
+
onSelect={onSelect}
|
|
1693
|
+
options={options}
|
|
1694
|
+
/>);
|
|
1695
|
+
const input = screen.getByRole('textbox');
|
|
1696
|
+
fireEvent.change(input, { target: { value: 'Can' } });
|
|
1697
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1698
|
+
expect(onInputChange).toHaveBeenLastCalledWith('Can', { reason: 'input' });
|
|
1699
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
1700
|
+
expect(onSelect).toHaveBeenCalledWith(null);
|
|
1701
|
+
expect(onInputChange).toHaveBeenLastCalledWith('', { reason: 'input' });
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
test('accepts externally loaded options without a loader or local filtering', () => {
|
|
1705
|
+
const onInputChange = jest.fn();
|
|
1706
|
+
const { rerender } = render(<Dropdown async
|
|
1707
|
+
autocomplete
|
|
1708
|
+
onInputChange={onInputChange}
|
|
1709
|
+
options={[]}
|
|
1710
|
+
/>);
|
|
1711
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'remote query' } });
|
|
1712
|
+
rerender(<Dropdown async
|
|
1713
|
+
autocomplete
|
|
1714
|
+
onInputChange={onInputChange}
|
|
1715
|
+
options={options}
|
|
1716
|
+
/>);
|
|
1717
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1718
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
1719
|
+
fireEvent.click(screen.getByText('Canada'));
|
|
1720
|
+
expect(onInputChange).toHaveBeenCalledTimes(1);
|
|
1721
|
+
expect(onInputChange).toHaveBeenLastCalledWith('remote query', { reason: 'input' });
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
describe('Dropdown input lifecycle', () => {
|
|
1728
|
+
test('query-only clear is opt-in and reports one clear notification', () => {
|
|
1729
|
+
const onInputChange = jest.fn();
|
|
1730
|
+
const { container, rerender } = render(<Dropdown autocomplete
|
|
1731
|
+
options={options}
|
|
1732
|
+
/>);
|
|
1733
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Can' } });
|
|
1734
|
+
expect(container.querySelector('[aria-label="times icon"]')).toBeNull();
|
|
1735
|
+
rerender(<Dropdown autocomplete
|
|
1736
|
+
onInputChange={onInputChange}
|
|
1737
|
+
options={options}
|
|
1738
|
+
/>);
|
|
1739
|
+
fireEvent.click(container.querySelector('[aria-label="times icon"]').closest('div'));
|
|
1740
|
+
expect(onInputChange).toHaveBeenCalledTimes(1);
|
|
1741
|
+
expect(onInputChange).toHaveBeenCalledWith('', { reason: 'clear' });
|
|
1742
|
+
expect(screen.getByRole('textbox')).toHaveValue('');
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
test('empty multi-select query preserves selected pills', () => {
|
|
1746
|
+
const onSelect = jest.fn();
|
|
1747
|
+
render(<Dropdown autocomplete
|
|
1748
|
+
defaultValue={[options[0]]}
|
|
1749
|
+
multiSelect
|
|
1750
|
+
onInputChange={jest.fn()}
|
|
1751
|
+
onSelect={onSelect}
|
|
1752
|
+
options={options}
|
|
1753
|
+
/>);
|
|
1754
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Can' } });
|
|
1755
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '' } });
|
|
1756
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1757
|
+
expect(screen.getByText('United States', { selector: '.pb_form_pill_tag' })).toBeInTheDocument();
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
describe('Dropdown initial options and caching', () => {
|
|
1763
|
+
beforeEach(() => jest.useFakeTimers());
|
|
1764
|
+
afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); });
|
|
1765
|
+
|
|
1766
|
+
test('defaultOptions true preloads without opening the menu', () => {
|
|
1767
|
+
const loadOptions = jest.fn();
|
|
1768
|
+
const { container } = render(<Dropdown async
|
|
1769
|
+
autocomplete
|
|
1770
|
+
defaultOptions
|
|
1771
|
+
loadOptions={loadOptions}
|
|
1772
|
+
/>);
|
|
1773
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1774
|
+
expect(loadOptions.mock.calls[0][0]).toBe('');
|
|
1775
|
+
act(() => loadOptions.mock.calls[0][1](options));
|
|
1776
|
+
expect(container.querySelector('.pb_dropdown_container')).toHaveClass('close');
|
|
1777
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
test('an initial array needs no request', () => {
|
|
1781
|
+
const loadOptions = jest.fn();
|
|
1782
|
+
render(<Dropdown async
|
|
1783
|
+
autocomplete
|
|
1784
|
+
defaultOptions={options}
|
|
1785
|
+
loadOptions={loadOptions}
|
|
1786
|
+
/>);
|
|
1787
|
+
act(() => jest.advanceTimersByTime(1000));
|
|
1788
|
+
expect(loadOptions).not.toHaveBeenCalled();
|
|
1789
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
test.each([false, true])('search reuse is gated by cacheOptions=%s', (cacheOptions) => {
|
|
1793
|
+
const loadOptions = jest.fn();
|
|
1794
|
+
render(<Dropdown async
|
|
1795
|
+
autocomplete
|
|
1796
|
+
cacheOptions={cacheOptions}
|
|
1797
|
+
loadOptions={loadOptions}
|
|
1798
|
+
/>);
|
|
1799
|
+
const input = screen.getByRole('textbox');
|
|
1800
|
+
fireEvent.change(input, { target: { value: 'can' } });
|
|
1801
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1802
|
+
act(() => loadOptions.mock.calls[0][1](options));
|
|
1803
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
1804
|
+
fireEvent.change(input, { target: { value: 'can' } });
|
|
1805
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1806
|
+
expect(loadOptions).toHaveBeenCalledTimes(cacheOptions ? 1 : 2);
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
describe('Dropdown cache isolation and opt-in', () => {
|
|
1812
|
+
beforeEach(() => jest.useFakeTimers());
|
|
1813
|
+
afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); });
|
|
1814
|
+
|
|
1815
|
+
test('initial loading and caching do nothing without async', () => {
|
|
1816
|
+
const loadOptions = jest.fn();
|
|
1817
|
+
render(<Dropdown autocomplete
|
|
1818
|
+
cacheOptions
|
|
1819
|
+
defaultOptions
|
|
1820
|
+
loadOptions={loadOptions}
|
|
1821
|
+
options={options}
|
|
1822
|
+
/>);
|
|
1823
|
+
act(() => jest.advanceTimersByTime(250));
|
|
1824
|
+
expect(loadOptions).not.toHaveBeenCalled();
|
|
1825
|
+
expect(screen.getByText('Canada')).toBeInTheDocument();
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
test('errors are retried rather than cached', async () => {
|
|
1829
|
+
const loadOptions = jest.fn().mockRejectedValue(new Error('Failed'));
|
|
1830
|
+
render(<Dropdown async
|
|
1831
|
+
autocomplete
|
|
1832
|
+
cacheOptions
|
|
1833
|
+
loadOptions={loadOptions}
|
|
1834
|
+
/>);
|
|
1835
|
+
const input = screen.getByRole('textbox');
|
|
1836
|
+
fireEvent.change(input, { target: { value: 'can' } });
|
|
1837
|
+
await act(async () => { jest.advanceTimersByTime(250); });
|
|
1838
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
1839
|
+
fireEvent.change(input, { target: { value: 'can' } });
|
|
1840
|
+
await act(async () => { jest.advanceTimersByTime(250); });
|
|
1841
|
+
expect(loadOptions).toHaveBeenCalledTimes(2);
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
describe('Dropdown equivalent controlled values', () => {
|
|
1847
|
+
test.each([false, true])('parent rerenders preserve the query (multiSelect: %s)', (multiSelect) => {
|
|
1848
|
+
const Example = () => {
|
|
1849
|
+
const [, setQuery] = useState('');
|
|
1850
|
+
return (
|
|
1851
|
+
<Dropdown autocomplete
|
|
1852
|
+
multiSelect={multiSelect}
|
|
1853
|
+
onInputChange={setQuery}
|
|
1854
|
+
options={options}
|
|
1855
|
+
value={multiSelect ? [{ ...options[0] }] : { ...options[0] }}
|
|
1856
|
+
/>
|
|
1857
|
+
);
|
|
1858
|
+
};
|
|
1859
|
+
render(<Example />);
|
|
1860
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Can' } });
|
|
1861
|
+
expect(screen.getByRole('textbox')).toHaveValue('Can');
|
|
1862
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Cana' } });
|
|
1863
|
+
expect(screen.getByRole('textbox')).toHaveValue('Cana');
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
test('real identity, label and empty-selection changes still synchronize the input', () => {
|
|
1867
|
+
const onSelect = jest.fn();
|
|
1868
|
+
const props = { autocomplete: true, options, onSelect };
|
|
1869
|
+
const { rerender } = render(<Dropdown {...props}
|
|
1870
|
+
value={options[0]}
|
|
1871
|
+
/>);
|
|
1872
|
+
const input = screen.getByRole('textbox');
|
|
1873
|
+
fireEvent.change(input, { target: { value: 'query' } });
|
|
1874
|
+
rerender(<Dropdown {...props}
|
|
1875
|
+
value={{ ...options[0], extraMetadata: true }}
|
|
1876
|
+
/>);
|
|
1877
|
+
expect(input).toHaveValue('query');
|
|
1878
|
+
rerender(<Dropdown {...props}
|
|
1879
|
+
value={{ ...options[0], id: 'different' }}
|
|
1880
|
+
/>);
|
|
1881
|
+
expect(input).toHaveValue('United States');
|
|
1882
|
+
rerender(<Dropdown {...props}
|
|
1883
|
+
value={{ ...options[0], id: 'different', label: 'Updated label' }}
|
|
1884
|
+
/>);
|
|
1885
|
+
expect(input).toHaveValue('Updated label');
|
|
1886
|
+
rerender(<Dropdown {...props}
|
|
1887
|
+
value={null}
|
|
1888
|
+
/>);
|
|
1889
|
+
expect(input).toHaveValue('');
|
|
1890
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1891
|
+
});
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
test('async multi-select identity avoids scanning selections for every option', () => {
|
|
1895
|
+
let reads = 0;
|
|
1896
|
+
const manyOptions = Array.from({ length: 120 }, (_, id) => {
|
|
1897
|
+
const option = { id, label: `Option ${id}` };
|
|
1898
|
+
Object.defineProperty(option, 'value', { enumerable: true, get: () => { reads += 1; return id; } });
|
|
1899
|
+
return option;
|
|
1900
|
+
});
|
|
1901
|
+
const { container } = render(
|
|
1902
|
+
<Dropdown async
|
|
1903
|
+
defaultValue={manyOptions.slice(0, 60)}
|
|
1904
|
+
isClosed={false}
|
|
1905
|
+
multiSelect
|
|
1906
|
+
options={manyOptions}
|
|
1907
|
+
/>
|
|
1908
|
+
);
|
|
1909
|
+
expect(container.querySelectorAll('.pb_dropdown_container li')).toHaveLength(60);
|
|
1910
|
+
// Allow normal render passes while rejecting the quadratic pairwise scan.
|
|
1911
|
+
expect(reads).toBeLessThan(2000);
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
describe('Dropdown Rails input parity', () => {
|
|
1915
|
+
test.each([false, true])('empty autocomplete clears selection without a listener only in async mode: %s', (async) => {
|
|
1916
|
+
const onSelect = jest.fn();
|
|
1917
|
+
const ref = React.createRef();
|
|
1918
|
+
render(<Dropdown async={async}
|
|
1919
|
+
autocomplete
|
|
1920
|
+
defaultValue={options[0]}
|
|
1921
|
+
onSelect={onSelect}
|
|
1922
|
+
options={options}
|
|
1923
|
+
ref={ref}
|
|
1924
|
+
/>);
|
|
1925
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } });
|
|
1926
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1927
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '' } });
|
|
1928
|
+
if (async) expect(onSelect).toHaveBeenCalledWith(null);
|
|
1929
|
+
else expect(onSelect).not.toHaveBeenCalled();
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
test('empty async multi-select query preserves selection without an input listener', () => {
|
|
1933
|
+
const onSelect = jest.fn();
|
|
1934
|
+
render(<Dropdown async
|
|
1935
|
+
autocomplete
|
|
1936
|
+
defaultValue={[options[0]]}
|
|
1937
|
+
multiSelect
|
|
1938
|
+
onSelect={onSelect}
|
|
1939
|
+
options={options}
|
|
1940
|
+
/>);
|
|
1941
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } });
|
|
1942
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '' } });
|
|
1943
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1944
|
+
expect(screen.getByText('United States', { selector: '.pb_form_pill_tag' })).toBeInTheDocument();
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
test('empty async searchbar query preserves selection', () => {
|
|
1948
|
+
const onSelect = jest.fn();
|
|
1949
|
+
render(<Dropdown async
|
|
1950
|
+
defaultValue={options[0]}
|
|
1951
|
+
onSelect={onSelect}
|
|
1952
|
+
options={options}
|
|
1953
|
+
>
|
|
1954
|
+
<Dropdown.Trigger />
|
|
1955
|
+
<Dropdown.Container searchbar />
|
|
1956
|
+
</Dropdown>);
|
|
1957
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'query' } });
|
|
1958
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '' } });
|
|
1959
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1960
|
+
});
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
describe('Dropdown migration-only async contract', () => {
|
|
1964
|
+
test('passes short queries to the application loader and keeps full result data', () => {
|
|
1965
|
+
jest.useFakeTimers();
|
|
1966
|
+
const loadOptions = jest.fn();
|
|
1967
|
+
const onSelect = jest.fn();
|
|
1968
|
+
const { unmount } = render(<Dropdown async
|
|
1969
|
+
autocomplete
|
|
1970
|
+
loadOptions={loadOptions}
|
|
1971
|
+
onSelect={onSelect}
|
|
1972
|
+
/>);
|
|
1973
|
+
expect(loadOptions).not.toHaveBeenCalled();
|
|
1974
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'a' } });
|
|
1975
|
+
act(() => jest.advanceTimersByTime(0));
|
|
1976
|
+
expect(loadOptions).toHaveBeenCalledTimes(1);
|
|
1977
|
+
expect(loadOptions.mock.calls[0][0]).toBe('a');
|
|
1978
|
+
expect(loadOptions.mock.calls[0]).toHaveLength(2);
|
|
1979
|
+
act(() => loadOptions.mock.calls[0][1]([options[0]]));
|
|
1980
|
+
fireEvent.click(screen.getByText('United States'));
|
|
1981
|
+
expect(onSelect).toHaveBeenCalledWith(options[0]);
|
|
1982
|
+
unmount();
|
|
1983
|
+
jest.clearAllTimers();
|
|
1984
|
+
jest.useRealTimers();
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
test('native form reset does not introduce React selection callbacks', () => {
|
|
1988
|
+
const onSelect = jest.fn();
|
|
1989
|
+
const onInputChange = jest.fn();
|
|
1990
|
+
const { container } = render(<form><Dropdown async
|
|
1991
|
+
autocomplete
|
|
1992
|
+
defaultValue={options[0]}
|
|
1993
|
+
onInputChange={onInputChange}
|
|
1994
|
+
onSelect={onSelect}
|
|
1995
|
+
options={options}
|
|
1996
|
+
/></form>);
|
|
1997
|
+
fireEvent.reset(container.querySelector('form'));
|
|
1998
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
1999
|
+
expect(onInputChange).not.toHaveBeenCalled();
|
|
2000
|
+
});
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
describe('async Dropdown Typeahead parity', () => {
|
|
2004
|
+
beforeEach(() => jest.useFakeTimers());
|
|
2005
|
+
afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); });
|
|
2006
|
+
|
|
2007
|
+
const people = [
|
|
2008
|
+
{ label: 'Ann Lee', value: 1 },
|
|
2009
|
+
{ label: 'Ann Lee', value: 2 },
|
|
2010
|
+
{ label: 'Bob Ray', value: 3 },
|
|
2011
|
+
];
|
|
2012
|
+
const flush = async () => { await act(async () => { jest.advanceTimersByTime(0); }); };
|
|
2013
|
+
const optionTexts = () => Array.from(document.querySelectorAll('.pb_dropdown_container .pb_body_kit'))
|
|
2014
|
+
.map((element) => element.textContent);
|
|
2015
|
+
|
|
2016
|
+
test('restores preloaded defaultOptions when the query is emptied', async () => {
|
|
2017
|
+
const loadOptions = jest.fn((term) => Promise.resolve(people.filter((person) => person.label.toLowerCase().includes(term))));
|
|
2018
|
+
render(<Dropdown async
|
|
2019
|
+
autocomplete
|
|
2020
|
+
defaultOptions
|
|
2021
|
+
loadOptions={loadOptions}
|
|
2022
|
+
/>);
|
|
2023
|
+
const input = screen.getByRole('textbox');
|
|
2024
|
+
await flush();
|
|
2025
|
+
fireEvent.change(input, { target: { value: 'bob' } });
|
|
2026
|
+
await flush();
|
|
2027
|
+
expect(optionTexts()).toEqual(['Bob Ray']);
|
|
2028
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
2029
|
+
await flush();
|
|
2030
|
+
expect(optionTexts()).toEqual(['Ann Lee', 'Ann Lee', 'Bob Ray']);
|
|
2031
|
+
expect(loadOptions).toHaveBeenCalledTimes(2);
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
test('restores a defaultOptions array after clearing and after selecting', async () => {
|
|
2035
|
+
const loadOptions = jest.fn(() => Promise.resolve([people[2]]));
|
|
2036
|
+
render(<Dropdown async
|
|
2037
|
+
autocomplete
|
|
2038
|
+
defaultOptions={people.slice(0, 1)}
|
|
2039
|
+
loadOptions={loadOptions}
|
|
2040
|
+
multiSelect
|
|
2041
|
+
/>);
|
|
2042
|
+
const input = screen.getByRole('textbox');
|
|
2043
|
+
fireEvent.change(input, { target: { value: 'bob' } });
|
|
2044
|
+
await flush();
|
|
2045
|
+
expect(optionTexts()).toEqual(['Bob Ray']);
|
|
2046
|
+
fireEvent.click(screen.getByText('Bob Ray'));
|
|
2047
|
+
fireEvent.click(input);
|
|
2048
|
+
expect(optionTexts()).toEqual(['Ann Lee']);
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
test('focuses the first loaded result so Enter selects it', async () => {
|
|
2052
|
+
const onSelect = jest.fn();
|
|
2053
|
+
render(<Dropdown async
|
|
2054
|
+
autocomplete
|
|
2055
|
+
loadOptions={() => Promise.resolve(people)}
|
|
2056
|
+
onSelect={onSelect}
|
|
2057
|
+
/>);
|
|
2058
|
+
const input = screen.getByRole('textbox');
|
|
2059
|
+
fireEvent.click(input);
|
|
2060
|
+
fireEvent.change(input, { target: { value: 'a' } });
|
|
2061
|
+
await flush();
|
|
2062
|
+
fireEvent.keyDown(input, { key: 'Enter' });
|
|
2063
|
+
expect(onSelect).toHaveBeenCalledWith(people[0]);
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
test('focuses externally loaded options when they arrive after the last keystroke', () => {
|
|
2067
|
+
const onSelect = jest.fn();
|
|
2068
|
+
const { rerender } = render(<Dropdown async
|
|
2069
|
+
autocomplete
|
|
2070
|
+
onSelect={onSelect}
|
|
2071
|
+
options={[]}
|
|
2072
|
+
/>);
|
|
2073
|
+
const input = screen.getByRole('textbox');
|
|
2074
|
+
fireEvent.change(input, { target: { value: 'an' } });
|
|
2075
|
+
fireEvent.keyDown(input, { key: 'Enter' });
|
|
2076
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
2077
|
+
|
|
2078
|
+
rerender(<Dropdown async
|
|
2079
|
+
autocomplete
|
|
2080
|
+
onSelect={onSelect}
|
|
2081
|
+
options={people}
|
|
2082
|
+
/>);
|
|
2083
|
+
fireEvent.keyDown(input, { key: 'Enter' });
|
|
2084
|
+
expect(onSelect).toHaveBeenCalledWith(people[0]);
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
test('ArrowDown still moves past the first externally loaded result', () => {
|
|
2088
|
+
const onSelect = jest.fn();
|
|
2089
|
+
const { rerender } = render(<Dropdown async
|
|
2090
|
+
autocomplete
|
|
2091
|
+
onSelect={onSelect}
|
|
2092
|
+
options={[]}
|
|
2093
|
+
/>);
|
|
2094
|
+
const input = screen.getByRole('textbox');
|
|
2095
|
+
fireEvent.change(input, { target: { value: 'an' } });
|
|
2096
|
+
rerender(<Dropdown async
|
|
2097
|
+
autocomplete
|
|
2098
|
+
onSelect={onSelect}
|
|
2099
|
+
options={people}
|
|
2100
|
+
/>);
|
|
2101
|
+
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
|
2102
|
+
fireEvent.keyDown(input, { key: 'Enter' });
|
|
2103
|
+
expect(onSelect).toHaveBeenCalledWith(people[1]);
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
test('keys results without ids by value', async () => {
|
|
2107
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
2108
|
+
render(<Dropdown async
|
|
2109
|
+
autocomplete
|
|
2110
|
+
loadOptions={() => Promise.resolve(people)}
|
|
2111
|
+
/>);
|
|
2112
|
+
const input = screen.getByRole('textbox');
|
|
2113
|
+
fireEvent.click(input);
|
|
2114
|
+
fireEvent.change(input, { target: { value: 'a' } });
|
|
2115
|
+
await flush();
|
|
2116
|
+
expect(consoleError.mock.calls.some(([message]) => String(message).includes('unique "key"'))).toBe(false);
|
|
2117
|
+
consoleError.mockRestore();
|
|
2118
|
+
});
|
|
2119
|
+
|
|
2120
|
+
test('identifies results by value so same-label options stay distinct', async () => {
|
|
2121
|
+
const onSelect = jest.fn();
|
|
2122
|
+
render(<Dropdown async
|
|
2123
|
+
autocomplete
|
|
2124
|
+
loadOptions={() => Promise.resolve(people)}
|
|
2125
|
+
multiSelect
|
|
2126
|
+
onSelect={onSelect}
|
|
2127
|
+
/>);
|
|
2128
|
+
const input = screen.getByRole('textbox');
|
|
2129
|
+
fireEvent.change(input, { target: { value: 'ann' } });
|
|
2130
|
+
await flush();
|
|
2131
|
+
fireEvent.click(screen.getAllByText('Ann Lee')[0]);
|
|
2132
|
+
expect(onSelect).toHaveBeenLastCalledWith([people[0]]);
|
|
2133
|
+
fireEvent.change(input, { target: { value: 'ann lee' } });
|
|
2134
|
+
await flush();
|
|
2135
|
+
expect(optionTexts()).toEqual(['Ann Lee', 'Bob Ray']);
|
|
2136
|
+
fireEvent.click(screen.getByText('Ann Lee', { selector: '.pb_dropdown_container .pb_body_kit' }));
|
|
2137
|
+
expect(onSelect).toHaveBeenLastCalledWith([people[0], people[1]]);
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
test.each([false, true])('single autocomplete shows the selected label in the input (controlled: %s)', async (controlled) => {
|
|
2141
|
+
const Example = () => {
|
|
2142
|
+
const [selected, setSelected] = useState(null);
|
|
2143
|
+
return (
|
|
2144
|
+
<Dropdown async
|
|
2145
|
+
autocomplete
|
|
2146
|
+
loadOptions={() => Promise.resolve(people)}
|
|
2147
|
+
onSelect={setSelected}
|
|
2148
|
+
{...(controlled ? { value: selected } : {})}
|
|
2149
|
+
/>
|
|
2150
|
+
);
|
|
2151
|
+
};
|
|
2152
|
+
render(<Example />);
|
|
2153
|
+
const input = screen.getByRole('textbox');
|
|
2154
|
+
fireEvent.click(input);
|
|
2155
|
+
fireEvent.change(input, { target: { value: 'bob' } });
|
|
2156
|
+
await flush();
|
|
2157
|
+
fireEvent.click(screen.getByText('Bob Ray'));
|
|
2158
|
+
expect(input).toHaveValue('Bob Ray');
|
|
2159
|
+
fireEvent.click(input);
|
|
2160
|
+
expect(optionTexts()).toEqual(['Bob Ray']);
|
|
2161
|
+
expect(screen.queryByText('No results found')).not.toBeInTheDocument();
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
test('reopening after selection shows only labels matching the input', async () => {
|
|
2165
|
+
render(<Dropdown async
|
|
2166
|
+
autocomplete
|
|
2167
|
+
loadOptions={() => Promise.resolve([
|
|
2168
|
+
{ label: 'Ada Lovelace', value: 1 },
|
|
2169
|
+
{ label: 'Adam Smith', value: 2 },
|
|
2170
|
+
])}
|
|
2171
|
+
/>);
|
|
2172
|
+
const input = screen.getByRole('textbox');
|
|
2173
|
+
fireEvent.change(input, { target: { value: 'ad' } });
|
|
2174
|
+
await flush();
|
|
2175
|
+
fireEvent.click(screen.getByText('Ada Lovelace'));
|
|
2176
|
+
fireEvent.click(input);
|
|
2177
|
+
expect(optionTexts()).toEqual(['Ada Lovelace']);
|
|
2178
|
+
expect(screen.queryByText('No results found')).not.toBeInTheDocument();
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
test('keeps earlier label matches when a query after a space returns nothing', async () => {
|
|
2182
|
+
const people = [
|
|
2183
|
+
{ label: 'Emily Smith', value: 1 },
|
|
2184
|
+
{ label: 'Emily Jones', value: 2 },
|
|
2185
|
+
];
|
|
2186
|
+
const loadOptions = (term) => Promise.resolve(
|
|
2187
|
+
term.trim().includes(' ') && !term.endsWith(' ') ? [] : people
|
|
2188
|
+
);
|
|
2189
|
+
render(<Dropdown async
|
|
2190
|
+
autocomplete
|
|
2191
|
+
loadOptions={loadOptions}
|
|
2192
|
+
/>);
|
|
2193
|
+
const input = screen.getByRole('textbox');
|
|
2194
|
+
fireEvent.change(input, { target: { value: 'Emily' } });
|
|
2195
|
+
await flush();
|
|
2196
|
+
fireEvent.change(input, { target: { value: 'Emily ' } });
|
|
2197
|
+
await flush();
|
|
2198
|
+
expect(optionTexts()).toEqual(['Emily Smith', 'Emily Jones']);
|
|
2199
|
+
fireEvent.change(input, { target: { value: 'Emily S' } });
|
|
2200
|
+
await flush();
|
|
2201
|
+
expect(optionTexts()).toEqual(['Emily Smith']);
|
|
2202
|
+
expect(screen.queryByText('No results found')).not.toBeInTheDocument();
|
|
2203
|
+
fireEvent.change(input, { target: { value: 'Emily Z' } });
|
|
2204
|
+
await flush();
|
|
2205
|
+
expect(screen.getByText('No results found')).toBeInTheDocument();
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
test('cacheOptions stores the empty server response, not locally kept rows', async () => {
|
|
2209
|
+
const people = [
|
|
2210
|
+
{ label: 'Emily Smith', value: 1 },
|
|
2211
|
+
{ label: 'Emily Jones', value: 2 },
|
|
2212
|
+
];
|
|
2213
|
+
const loadOptions = jest.fn((term) => Promise.resolve(
|
|
2214
|
+
term.trim().includes(' ') && !term.endsWith(' ') ? [] : people
|
|
2215
|
+
));
|
|
2216
|
+
render(<Dropdown async
|
|
2217
|
+
autocomplete
|
|
2218
|
+
cacheOptions
|
|
2219
|
+
loadOptions={loadOptions}
|
|
2220
|
+
/>);
|
|
2221
|
+
const input = screen.getByRole('textbox');
|
|
2222
|
+
fireEvent.change(input, { target: { value: 'Emily' } });
|
|
2223
|
+
await flush();
|
|
2224
|
+
fireEvent.change(input, { target: { value: 'Emily S' } });
|
|
2225
|
+
await flush();
|
|
2226
|
+
expect(optionTexts()).toEqual(['Emily Smith']);
|
|
2227
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
2228
|
+
fireEvent.change(input, { target: { value: 'Emily S' } });
|
|
2229
|
+
await flush();
|
|
2230
|
+
expect(screen.getByText('No results found')).toBeInTheDocument();
|
|
2231
|
+
expect(loadOptions).toHaveBeenCalledTimes(2);
|
|
2232
|
+
});
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
test('async results render through Dropdown.Container with a custom trigger display', async () => {
|
|
2236
|
+
jest.useFakeTimers();
|
|
2237
|
+
const Example = () => {
|
|
2238
|
+
const [selected, setSelected] = useState(null);
|
|
2239
|
+
return (
|
|
2240
|
+
<Dropdown async
|
|
2241
|
+
autocomplete
|
|
2242
|
+
loadOptions={() => Promise.resolve([{ label: 'Ada Lovelace', value: 7, title: 'Engineer' }])}
|
|
2243
|
+
onSelect={setSelected}
|
|
2244
|
+
value={selected}
|
|
2245
|
+
>
|
|
2246
|
+
<Dropdown.Trigger customDisplay={selected && <span>{selected.title}</span>} />
|
|
2247
|
+
<Dropdown.Container />
|
|
2248
|
+
</Dropdown>
|
|
2249
|
+
);
|
|
2250
|
+
};
|
|
2251
|
+
render(<Example />);
|
|
2252
|
+
const input = screen.getByRole('textbox');
|
|
2253
|
+
fireEvent.click(input);
|
|
2254
|
+
fireEvent.change(input, { target: { value: 'ada' } });
|
|
2255
|
+
await act(async () => { jest.advanceTimersByTime(0); });
|
|
2256
|
+
fireEvent.click(screen.getByText('Ada Lovelace'));
|
|
2257
|
+
expect(screen.getByText('Engineer')).toBeInTheDocument();
|
|
2258
|
+
jest.useRealTimers();
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
describe('Dropdown noOptionsText', () => {
|
|
2262
|
+
test('replaces the empty message for filtered options', () => {
|
|
2263
|
+
render(<Dropdown autocomplete
|
|
2264
|
+
noOptionsText="No countries match"
|
|
2265
|
+
options={options}
|
|
2266
|
+
/>);
|
|
2267
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'zzz' } });
|
|
2268
|
+
expect(screen.getByText('No countries match')).toBeInTheDocument();
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
test('replaces the empty message for async results', async () => {
|
|
2272
|
+
jest.useFakeTimers();
|
|
2273
|
+
render(<Dropdown async
|
|
2274
|
+
autocomplete
|
|
2275
|
+
loadOptions={() => Promise.resolve([])}
|
|
2276
|
+
noOptionsText="No agents available"
|
|
2277
|
+
/>);
|
|
2278
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'zzz' } });
|
|
2279
|
+
await act(async () => { jest.advanceTimersByTime(0); });
|
|
2280
|
+
expect(screen.getByRole('status')).toHaveTextContent('No agents available');
|
|
2281
|
+
jest.useRealTimers();
|
|
2282
|
+
});
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
describe('async Dropdown empty menu', () => {
|
|
2286
|
+
test.each([
|
|
2287
|
+
['loadOptions', { loadOptions: jest.fn() }],
|
|
2288
|
+
['external options', { options: [] }],
|
|
2289
|
+
])('opens with the no results message before searching (%s)', (_mode, props) => {
|
|
2290
|
+
const { container } = render(<Dropdown async
|
|
2291
|
+
autocomplete
|
|
2292
|
+
{...props}
|
|
2293
|
+
/>);
|
|
2294
|
+
fireEvent.click(screen.getByRole('textbox'));
|
|
2295
|
+
expect(container.querySelector('.pb_dropdown_container')).toHaveClass('open');
|
|
2296
|
+
expect(screen.getByRole('status')).toHaveTextContent('No results found');
|
|
2297
|
+
});
|
|
2298
|
+
|
|
2299
|
+
test('uses noOptionsText for the empty menu', () => {
|
|
2300
|
+
render(<Dropdown async
|
|
2301
|
+
autocomplete
|
|
2302
|
+
loadOptions={jest.fn()}
|
|
2303
|
+
noOptionsText="Type to search users"
|
|
2304
|
+
/>);
|
|
2305
|
+
fireEvent.click(screen.getByRole('textbox'));
|
|
2306
|
+
expect(screen.getByRole('status')).toHaveTextContent('Type to search users');
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
test('does not show an empty message when every result is already selected', async () => {
|
|
2310
|
+
jest.useFakeTimers();
|
|
2311
|
+
const selected = [{ label: 'Ann Lee', value: 1 }];
|
|
2312
|
+
render(<Dropdown async
|
|
2313
|
+
autocomplete
|
|
2314
|
+
defaultValue={selected}
|
|
2315
|
+
loadOptions={() => Promise.resolve([selected[0]])}
|
|
2316
|
+
multiSelect
|
|
2317
|
+
/>);
|
|
2318
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'ann' } });
|
|
2319
|
+
await act(async () => { jest.advanceTimersByTime(0); });
|
|
2320
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
2321
|
+
expect(screen.queryByText('No results found')).not.toBeInTheDocument();
|
|
2322
|
+
jest.useRealTimers();
|
|
2323
|
+
});
|
|
2324
|
+
|
|
2325
|
+
test('selecting the last remaining option does not show an empty message', () => {
|
|
2326
|
+
const people = [
|
|
2327
|
+
{ label: 'Ann Lee', value: 1 },
|
|
2328
|
+
{ label: 'Bob Ray', value: 2 },
|
|
2329
|
+
];
|
|
2330
|
+
const { container } = render(<Dropdown async
|
|
2331
|
+
autocomplete
|
|
2332
|
+
closeOnClick="outside"
|
|
2333
|
+
multiSelect
|
|
2334
|
+
options={people}
|
|
2335
|
+
/>);
|
|
2336
|
+
fireEvent.click(screen.getByRole('textbox'));
|
|
2337
|
+
fireEvent.click(screen.getByText('Ann Lee'));
|
|
2338
|
+
expect(container.querySelector('.pb_dropdown_container')).toHaveClass('open');
|
|
2339
|
+
fireEvent.click(screen.getByText('Bob Ray'));
|
|
2340
|
+
expect(container.querySelector('.pb_dropdown_container')).toHaveClass('open');
|
|
2341
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
2342
|
+
expect(screen.queryByText('No results found')).not.toBeInTheDocument();
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
test('an empty async search shows the empty message instead of blankSelection', async () => {
|
|
2346
|
+
jest.useFakeTimers();
|
|
2347
|
+
render(<Dropdown async
|
|
2348
|
+
autocomplete
|
|
2349
|
+
blankSelection="Select one..."
|
|
2350
|
+
loadOptions={() => Promise.resolve([])}
|
|
2351
|
+
/>);
|
|
2352
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'zzz' } });
|
|
2353
|
+
await act(async () => { jest.advanceTimersByTime(0); });
|
|
2354
|
+
expect(screen.getByRole('status')).toHaveTextContent('No results found');
|
|
2355
|
+
expect(screen.queryByText('Select one...')).not.toBeInTheDocument();
|
|
2356
|
+
jest.useRealTimers();
|
|
2357
|
+
});
|
|
2358
|
+
|
|
2359
|
+
test('blankSelection stays available when an async search has results', async () => {
|
|
2360
|
+
jest.useFakeTimers();
|
|
2361
|
+
render(<Dropdown async
|
|
2362
|
+
autocomplete
|
|
2363
|
+
blankSelection="Select one..."
|
|
2364
|
+
loadOptions={() => Promise.resolve([{ label: 'Ann Lee', value: 1 }])}
|
|
2365
|
+
/>);
|
|
2366
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'ann' } });
|
|
2367
|
+
await act(async () => { jest.advanceTimersByTime(0); });
|
|
2368
|
+
expect(screen.getByText('Select one...')).toBeInTheDocument();
|
|
2369
|
+
expect(screen.getByText('Ann Lee')).toBeInTheDocument();
|
|
2370
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
2371
|
+
jest.useRealTimers();
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
test('shows preloaded defaultOptions instead of the message', async () => {
|
|
2375
|
+
jest.useFakeTimers();
|
|
2376
|
+
render(<Dropdown async
|
|
2377
|
+
autocomplete
|
|
2378
|
+
defaultOptions
|
|
2379
|
+
loadOptions={() => Promise.resolve([{ label: 'Ada Lovelace', value: 1 }])}
|
|
2380
|
+
/>);
|
|
2381
|
+
await act(async () => { jest.advanceTimersByTime(0); });
|
|
2382
|
+
fireEvent.click(screen.getByRole('textbox'));
|
|
2383
|
+
expect(screen.getByText('Ada Lovelace')).toBeInTheDocument();
|
|
2384
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument();
|
|
2385
|
+
jest.useRealTimers();
|
|
2386
|
+
});
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
|
|
2390
|
+
describe('autocomplete selection replacement', () => {
|
|
2391
|
+
beforeEach(() => jest.useFakeTimers());
|
|
2392
|
+
afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); });
|
|
2393
|
+
|
|
2394
|
+
test.each(['async', 'controlled', 'input callback'])('%s replaces the selected label and searches with the first character', async (mode) => {
|
|
2395
|
+
const onInputChange = jest.fn();
|
|
2396
|
+
const onSelect = jest.fn();
|
|
2397
|
+
const loadOptions = jest.fn(() => Promise.resolve(options));
|
|
2398
|
+
const Example = () => {
|
|
2399
|
+
const [selected, setSelected] = useState(options[1]);
|
|
2400
|
+
return (
|
|
2401
|
+
<Dropdown async={mode === 'async'}
|
|
2402
|
+
autocomplete
|
|
2403
|
+
defaultValue={options[1]}
|
|
2404
|
+
loadOptions={loadOptions}
|
|
2405
|
+
onInputChange={onInputChange}
|
|
2406
|
+
onSelect={(option) => { onSelect(option); setSelected(option) }}
|
|
2407
|
+
options={options}
|
|
2408
|
+
{...(mode === 'controlled' ? { value: selected } : {})}
|
|
2409
|
+
/>
|
|
2410
|
+
);
|
|
2411
|
+
};
|
|
2412
|
+
render(<Example />);
|
|
2413
|
+
const input = screen.getByRole('textbox');
|
|
2414
|
+
expect(input).toHaveValue('Canada');
|
|
2415
|
+
expect(fireEvent.keyDown(input, { key: 'P' })).toBe(false);
|
|
2416
|
+
expect(input).toHaveValue('P');
|
|
2417
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
2418
|
+
expect(onSelect).toHaveBeenCalledWith(null);
|
|
2419
|
+
expect(onInputChange).toHaveBeenCalledTimes(1);
|
|
2420
|
+
expect(onInputChange).toHaveBeenCalledWith('P', { reason: 'input' });
|
|
2421
|
+
await act(async () => { jest.runOnlyPendingTimers(); });
|
|
2422
|
+
if (mode === 'async') expect(loadOptions).toHaveBeenCalledWith('P', expect.any(Function));
|
|
2423
|
+
fireEvent.change(input, { target: { value: 'Pa' } });
|
|
2424
|
+
expect(input).toHaveValue('Pa');
|
|
2425
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
test('the first character still replaces an unchanged selected label', () => {
|
|
2429
|
+
const onSelect = jest.fn();
|
|
2430
|
+
render(<Dropdown autocomplete
|
|
2431
|
+
defaultValue={options[1]}
|
|
2432
|
+
onSelect={onSelect}
|
|
2433
|
+
options={options}
|
|
2434
|
+
/>);
|
|
2435
|
+
const input = screen.getByRole('textbox');
|
|
2436
|
+
expect(fireEvent.keyDown(input, { key: 'P' })).toBe(false);
|
|
2437
|
+
expect(input).toHaveValue('P');
|
|
2438
|
+
expect(onSelect).toHaveBeenCalledWith(null);
|
|
2439
|
+
});
|
|
2440
|
+
|
|
2441
|
+
test('a character after backspace edits the query instead of replacing it', () => {
|
|
2442
|
+
render(<Dropdown autocomplete
|
|
2443
|
+
defaultValue={options[1]}
|
|
2444
|
+
options={options}
|
|
2445
|
+
/>);
|
|
2446
|
+
const input = screen.getByRole('textbox');
|
|
2447
|
+
expect(input).toHaveValue('Canada');
|
|
2448
|
+
expect(fireEvent.keyDown(input, { key: 'Backspace' })).toBe(true);
|
|
2449
|
+
fireEvent.change(input, { target: { value: 'Canad' } });
|
|
2450
|
+
expect(fireEvent.keyDown(input, { key: 'a' })).toBe(true);
|
|
2451
|
+
fireEvent.change(input, { target: { value: 'Canada' } });
|
|
2452
|
+
expect(input).toHaveValue('Canada');
|
|
2453
|
+
});
|
|
2454
|
+
|
|
2455
|
+
test.each(['Backspace', 'Delete'])('%s removes one character and keeps the selection until the query is empty', (key) => {
|
|
2456
|
+
const onSelect = jest.fn();
|
|
2457
|
+
const onInputChange = jest.fn();
|
|
2458
|
+
const Example = () => {
|
|
2459
|
+
const [selected, setSelected] = useState(options[1]);
|
|
2460
|
+
return (
|
|
2461
|
+
<Dropdown autocomplete
|
|
2462
|
+
onInputChange={onInputChange}
|
|
2463
|
+
onSelect={(option) => { onSelect(option); setSelected(option) }}
|
|
2464
|
+
options={options}
|
|
2465
|
+
value={selected}
|
|
2466
|
+
/>
|
|
2467
|
+
);
|
|
2468
|
+
};
|
|
2469
|
+
render(<Example />);
|
|
2470
|
+
const input = screen.getByRole('textbox');
|
|
2471
|
+
expect(fireEvent.keyDown(input, { key })).toBe(true);
|
|
2472
|
+
fireEvent.change(input, { target: { value: 'Canad' } });
|
|
2473
|
+
expect(input).toHaveValue('Canad');
|
|
2474
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
2475
|
+
expect(onInputChange).toHaveBeenCalledWith('Canad', { reason: 'input' });
|
|
2476
|
+
fireEvent.change(input, { target: { value: '' } });
|
|
2477
|
+
expect(onSelect).toHaveBeenCalledWith(null);
|
|
2478
|
+
expect(input).toHaveValue('');
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2481
|
+
test('async backspace removes one character and searches with the remainder', async () => {
|
|
2482
|
+
const loadOptions = jest.fn(() => Promise.resolve(options));
|
|
2483
|
+
render(<Dropdown async
|
|
2484
|
+
autocomplete
|
|
2485
|
+
defaultValue={options[1]}
|
|
2486
|
+
loadOptions={loadOptions}
|
|
2487
|
+
/>);
|
|
2488
|
+
const input = screen.getByRole('textbox');
|
|
2489
|
+
expect(input).toHaveValue('Canada');
|
|
2490
|
+
expect(fireEvent.keyDown(input, { key: 'Backspace' })).toBe(true);
|
|
2491
|
+
fireEvent.change(input, { target: { value: 'Canad' } });
|
|
2492
|
+
expect(input).toHaveValue('Canad');
|
|
2493
|
+
expect(screen.queryByText('Canada', { selector: '.pb_dropdown_trigger .pb_body_kit' })).not.toBeInTheDocument();
|
|
2494
|
+
await act(async () => { jest.advanceTimersByTime(0); });
|
|
2495
|
+
expect(loadOptions).toHaveBeenCalledWith('Canad', expect.any(Function));
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
test('a delayed controlled clear preserves the ongoing query', () => {
|
|
2499
|
+
const onSelect = jest.fn();
|
|
2500
|
+
const { rerender } = render(<Dropdown autocomplete
|
|
2501
|
+
onSelect={onSelect}
|
|
2502
|
+
options={options}
|
|
2503
|
+
value={options[1]}
|
|
2504
|
+
/>);
|
|
2505
|
+
const input = screen.getByRole('textbox');
|
|
2506
|
+
fireEvent.keyDown(input, { key: 'P' });
|
|
2507
|
+
fireEvent.change(input, { target: { value: 'Pa' } });
|
|
2508
|
+
expect(onSelect).toHaveBeenCalledWith(null);
|
|
2509
|
+
rerender(<Dropdown autocomplete
|
|
2510
|
+
onSelect={onSelect}
|
|
2511
|
+
options={options}
|
|
2512
|
+
value={null}
|
|
2513
|
+
/>);
|
|
2514
|
+
expect(input).toHaveValue('Pa');
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
test.each([
|
|
2518
|
+
{ key: 'a', ctrlKey: true },
|
|
2519
|
+
{ key: 'a', metaKey: true },
|
|
2520
|
+
{ key: 'a', altKey: true },
|
|
2521
|
+
{ key: 'a', isComposing: true },
|
|
2522
|
+
])('does not replace the selection for shortcut/composition events: %j', (event) => {
|
|
2523
|
+
const onSelect = jest.fn();
|
|
2524
|
+
render(<Dropdown autocomplete
|
|
2525
|
+
onSelect={onSelect}
|
|
2526
|
+
options={options}
|
|
2527
|
+
value={options[1]}
|
|
2528
|
+
/>);
|
|
2529
|
+
fireEvent.keyDown(screen.getByRole('textbox'), event);
|
|
2530
|
+
expect(screen.getByRole('textbox')).toHaveValue('Canada');
|
|
2531
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
test.each(['ArrowUp', 'ArrowDown', 'Tab', 'Escape', 'Home', 'End'])('%s preserves the selected label', (key) => {
|
|
2535
|
+
const onSelect = jest.fn();
|
|
2536
|
+
render(<Dropdown autocomplete
|
|
2537
|
+
onSelect={onSelect}
|
|
2538
|
+
options={options}
|
|
2539
|
+
value={options[1]}
|
|
2540
|
+
/>);
|
|
2541
|
+
fireEvent.keyDown(screen.getByRole('textbox'), { key });
|
|
2542
|
+
expect(screen.getByRole('textbox')).toHaveValue('Canada');
|
|
2543
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
2544
|
+
});
|
|
2545
|
+
});
|