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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, useLayoutEffect, forwardRef, useImperativeHandle, useMemo, useContext } from "react";
|
|
1
|
+
import React, { useState, useRef, useEffect, useLayoutEffect, forwardRef, useImperativeHandle, useMemo, useContext, useCallback } from "react";
|
|
2
2
|
import classnames from "classnames";
|
|
3
3
|
import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from "../utilities/props";
|
|
4
4
|
import { globalProps } from "../utilities/globalProps";
|
|
@@ -13,12 +13,14 @@ import DropdownContext from "./context";
|
|
|
13
13
|
import DropdownOption from "./subcomponents/DropdownOption";
|
|
14
14
|
import DropdownTrigger from "./subcomponents/DropdownTrigger";
|
|
15
15
|
import useDropdown from "./hooks/useDropdown";
|
|
16
|
+
import useAsyncOptions, { LoadOptions } from "./hooks/useAsyncOptions";
|
|
16
17
|
import getQuickPickOptions from "./quickpick";
|
|
17
18
|
|
|
18
19
|
import {
|
|
19
20
|
separateChildComponents,
|
|
20
21
|
prepareSubcomponents,
|
|
21
22
|
handleClickOutside,
|
|
23
|
+
setQuickPickDateOnPicker,
|
|
22
24
|
} from "./utilities";
|
|
23
25
|
|
|
24
26
|
import { DialogContext } from "../pb_dialog/_dialog_context";
|
|
@@ -81,9 +83,12 @@ function serializeDropdownFilterResetDefault(
|
|
|
81
83
|
return undefined;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
// Async results are server records, so identify them by value like Typeahead rather than by label.
|
|
87
|
+
const asyncOptionValue = (option: GenericObject) => option.value ?? option.id ?? option.label;
|
|
88
|
+
|
|
84
89
|
type CustomQuickPickDate = {
|
|
85
90
|
label: string;
|
|
86
|
-
value: string
|
|
91
|
+
value: Array<Date | string> | { timePeriod: string; amount: number };
|
|
87
92
|
};
|
|
88
93
|
|
|
89
94
|
type CustomQuickPickDates = {
|
|
@@ -91,8 +96,18 @@ type CustomQuickPickDates = {
|
|
|
91
96
|
dates: CustomQuickPickDate[];
|
|
92
97
|
};
|
|
93
98
|
|
|
99
|
+
type InputChangeReason = "input" | "clear";
|
|
100
|
+
|
|
94
101
|
type DropdownProps = {
|
|
95
102
|
aria?: { [key: string]: string };
|
|
103
|
+
async?: boolean;
|
|
104
|
+
loadOptions?: LoadOptions;
|
|
105
|
+
defaultOptions?: boolean | GenericObject[];
|
|
106
|
+
cacheOptions?: boolean;
|
|
107
|
+
loading?: boolean;
|
|
108
|
+
onInputChange?: (input: string, detail: { reason: InputChangeReason }) => void;
|
|
109
|
+
value?: GenericObject | GenericObject[] | null;
|
|
110
|
+
renderOption?: (option: GenericObject) => React.ReactNode;
|
|
96
111
|
autocomplete?: boolean;
|
|
97
112
|
blankSelection?: string;
|
|
98
113
|
children?: React.ReactChild[] | React.ReactChild | React.ReactElement[];
|
|
@@ -113,6 +128,7 @@ type DropdownProps = {
|
|
|
113
128
|
label?: string;
|
|
114
129
|
multiSelect?: boolean;
|
|
115
130
|
name?: string;
|
|
131
|
+
noOptionsText?: string;
|
|
116
132
|
onChange?: (event: { target: { name?: string; value: any } }) => void;
|
|
117
133
|
onSelect?: (arg: GenericObject) => null;
|
|
118
134
|
options?: GenericObject;
|
|
@@ -139,6 +155,14 @@ interface DropdownComponent
|
|
|
139
155
|
let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
140
156
|
const {
|
|
141
157
|
aria = {},
|
|
158
|
+
async = false,
|
|
159
|
+
loadOptions,
|
|
160
|
+
defaultOptions = false,
|
|
161
|
+
cacheOptions = false,
|
|
162
|
+
loading = false,
|
|
163
|
+
onInputChange,
|
|
164
|
+
renderOption,
|
|
165
|
+
value,
|
|
142
166
|
autocomplete = false,
|
|
143
167
|
blankSelection = '',
|
|
144
168
|
children,
|
|
@@ -159,6 +183,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
159
183
|
multiSelect = false,
|
|
160
184
|
formPillProps,
|
|
161
185
|
name,
|
|
186
|
+
noOptionsText,
|
|
162
187
|
onChange,
|
|
163
188
|
onSelect,
|
|
164
189
|
options,
|
|
@@ -188,15 +213,27 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
188
213
|
[classes],
|
|
189
214
|
);
|
|
190
215
|
|
|
216
|
+
const asyncEnabled = async && variant !== "quickpick";
|
|
217
|
+
const getOptionValue = asyncEnabled ? asyncOptionValue : undefined;
|
|
218
|
+
const { options: loadedOptions, status: asyncStatus, search: searchAsync, cancel: cancelAsync, resume: resumeAsync, clear: clearAsync } = useAsyncOptions(asyncEnabled && !!loadOptions && !disabled, loadOptions, defaultOptions, cacheOptions);
|
|
219
|
+
|
|
191
220
|
// ------------- Quick Pick ---------------------------------
|
|
192
221
|
// Use QuickPick options when variant is "quickpick"
|
|
193
222
|
const dropdownOptions = variant === "quickpick"
|
|
194
223
|
? getQuickPickOptions(rangeEndsToday, customQuickPickDates)
|
|
195
|
-
: (options || []);
|
|
224
|
+
: (asyncEnabled && loadOptions ? loadedOptions : options || []);
|
|
196
225
|
// ----------------------------------------------------------
|
|
197
226
|
|
|
198
227
|
const [isDropDownClosed, setIsDropDownClosed, toggleDropdown] = useDropdown(disabled ? true : isClosed);
|
|
199
228
|
|
|
229
|
+
const previouslyClosed = useRef(isDropDownClosed);
|
|
230
|
+
const queryRef = useRef("");
|
|
231
|
+
useLayoutEffect(() => {
|
|
232
|
+
if (asyncEnabled && (disabled || (!previouslyClosed.current && isDropDownClosed))) cancelAsync();
|
|
233
|
+
if (asyncEnabled && !disabled && previouslyClosed.current && !isDropDownClosed) resumeAsync(queryRef.current);
|
|
234
|
+
previouslyClosed.current = isDropDownClosed;
|
|
235
|
+
}, [asyncEnabled, isDropDownClosed, disabled, cancelAsync, resumeAsync]);
|
|
236
|
+
|
|
200
237
|
// Use a suffix for the trigger ID to avoid conflict with the outer div's id
|
|
201
238
|
const sanitizeForId = (str: string) =>
|
|
202
239
|
str.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
|
|
@@ -225,16 +262,53 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
225
262
|
return defaultValue || {};
|
|
226
263
|
}, [multiSelect, defaultValue, variant, dropdownOptions]);
|
|
227
264
|
|
|
228
|
-
const [
|
|
265
|
+
const [internalSelected, setInternalSelected] = useState<GenericObject | GenericObject[]>(
|
|
229
266
|
initialSelected
|
|
230
267
|
);
|
|
231
268
|
|
|
269
|
+
const optionKey = (option: GenericObject) => String(
|
|
270
|
+
getOptionValue ? getOptionValue(option) : option.id ?? option.value ?? option.label
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const isControlled = value !== undefined;
|
|
274
|
+
const selected = isControlled ? (value ?? (multiSelect ? [] : {})) : internalSelected;
|
|
275
|
+
const setSelected: React.Dispatch<React.SetStateAction<GenericObject | GenericObject[]>> = (next) => {
|
|
276
|
+
if (!isControlled) setInternalSelected(next);
|
|
277
|
+
};
|
|
278
|
+
const isSameOption = useCallback((left: GenericObject, right: GenericObject, legacyKey = "label") => {
|
|
279
|
+
if (!getOptionValue) return left?.[legacyKey] === right?.[legacyKey];
|
|
280
|
+
if (!left || !right || !Object.keys(left).length || !Object.keys(right).length) return false;
|
|
281
|
+
return String(getOptionValue(left)) === String(getOptionValue(right));
|
|
282
|
+
}, [getOptionValue]);
|
|
283
|
+
|
|
232
284
|
// Autocomplete displays the selection in the input; seed from defaultValue
|
|
233
285
|
const [filterItem, setFilterItem] = useState(() => {
|
|
234
286
|
if (!autocomplete || multiSelect) return "";
|
|
235
|
-
if (Array.isArray(
|
|
236
|
-
return (
|
|
287
|
+
if (Array.isArray(selected)) return "";
|
|
288
|
+
return (selected as GenericObject)?.label || "";
|
|
237
289
|
});
|
|
290
|
+
queryRef.current = filterItem;
|
|
291
|
+
|
|
292
|
+
const preserveQueryOnControlledClear = useRef(false);
|
|
293
|
+
|
|
294
|
+
// Form adapters may recreate equivalent option objects on every render.
|
|
295
|
+
// Synchronize the query only when selection identity or display text changes.
|
|
296
|
+
const controlledSelectionKey = isControlled ? JSON.stringify(
|
|
297
|
+
(Array.isArray(value) ? value : value && Object.keys(value).length ? [value] : [])
|
|
298
|
+
.map((option) => [
|
|
299
|
+
String(getOptionValue ? getOptionValue(option) : option.id ?? option.value ?? option.label),
|
|
300
|
+
option.label,
|
|
301
|
+
])
|
|
302
|
+
) : undefined;
|
|
303
|
+
const controlledLabel = !multiSelect && value && !Array.isArray(value) ? value?.label || "" : "";
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (isControlled && autocomplete) {
|
|
307
|
+
// Accepting a selection clear must not erase the character that started a new search.
|
|
308
|
+
if (controlledLabel || !preserveQueryOnControlledClear.current) setFilterItem(controlledLabel);
|
|
309
|
+
preserveQueryOnControlledClear.current = false;
|
|
310
|
+
}
|
|
311
|
+
}, [isControlled, controlledSelectionKey, controlledLabel, autocomplete, multiSelect]);
|
|
238
312
|
|
|
239
313
|
const filterResetDefaultSerialized = useMemo(
|
|
240
314
|
() => serializeDropdownFilterResetDefault(variant, multiSelect, defaultValue, dropdownOptions),
|
|
@@ -291,6 +365,12 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
291
365
|
? [selected]
|
|
292
366
|
: [];
|
|
293
367
|
|
|
368
|
+
const selectedOptionIds = useMemo(() => (
|
|
369
|
+
multiSelect && getOptionValue
|
|
370
|
+
? new Set(selectedArray.map((option) => String(getOptionValue(option))))
|
|
371
|
+
: null
|
|
372
|
+
), [multiSelect, getOptionValue, selectedArray]);
|
|
373
|
+
|
|
294
374
|
const { trigger, container, otherChildren } =
|
|
295
375
|
separateChildComponents(children);
|
|
296
376
|
|
|
@@ -321,33 +401,61 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
321
401
|
setIsDropDownClosed(disabled ? true : isClosed)
|
|
322
402
|
}, [disabled, isClosed])
|
|
323
403
|
|
|
324
|
-
|
|
404
|
+
// blankSelection is a clear action, not a search result, so it cannot stand in for an empty async list.
|
|
405
|
+
const blankSelectionOption: GenericObject = blankSelection && (!asyncEnabled || dropdownOptions.length > 0)
|
|
406
|
+
? [{ label: blankSelection, value: "" }]
|
|
407
|
+
: [];
|
|
325
408
|
const optionsWithBlankSelection = blankSelectionOption.concat(dropdownOptions);
|
|
326
409
|
|
|
327
410
|
const availableOptions = useMemo(()=> {
|
|
328
411
|
if (!multiSelect) return optionsWithBlankSelection;
|
|
329
|
-
|
|
330
|
-
|
|
412
|
+
if (selectedOptionIds) {
|
|
413
|
+
return optionsWithBlankSelection.filter((option: GenericObject) => !selectedOptionIds.has(String(getOptionValue(option))));
|
|
414
|
+
}
|
|
415
|
+
return optionsWithBlankSelection.filter((option: GenericObject) => !selectedArray.some((sel) => isSameOption(sel, option)));
|
|
416
|
+
}, [optionsWithBlankSelection, selectedArray, multiSelect, isSameOption, selectedOptionIds, getOptionValue]);
|
|
331
417
|
|
|
332
418
|
const filteredOptions = useMemo(() => {
|
|
333
|
-
// When the input shows the selected label, do not filter the list down to that one option
|
|
334
419
|
const selectedLabel =
|
|
335
420
|
!multiSelect &&
|
|
336
421
|
!Array.isArray(selected) &&
|
|
337
422
|
(selected as GenericObject)?.label;
|
|
423
|
+
// Async searches stay unfiltered until the input is showing the selected label.
|
|
424
|
+
// Reopening then shows only results that match that text.
|
|
425
|
+
if (asyncEnabled) {
|
|
426
|
+
if (!(autocomplete && selectedLabel && filterItem === selectedLabel)) return availableOptions;
|
|
427
|
+
const query = String(selectedLabel).toLowerCase();
|
|
428
|
+
return availableOptions.filter((opt: GenericObject) => String(opt.label).toLowerCase().includes(query));
|
|
429
|
+
}
|
|
430
|
+
// When the input shows the selected label, do not filter the list down to that one option
|
|
338
431
|
const filterText =
|
|
339
432
|
selectedLabel && filterItem === selectedLabel ? "" : filterItem;
|
|
340
433
|
return availableOptions.filter((opt: GenericObject) =>
|
|
341
434
|
String(opt.label).toLowerCase().includes(filterText.toLowerCase())
|
|
342
435
|
);
|
|
343
|
-
}, [availableOptions, filterItem, multiSelect, selected]);
|
|
436
|
+
}, [availableOptions, filterItem, multiSelect, selected, asyncEnabled, autocomplete]);
|
|
437
|
+
|
|
438
|
+
// Empty copy follows the settled search. Hiding already-selected rows must not turn a
|
|
439
|
+
// successful result list into "No results found", including the last pick while the menu stays open.
|
|
440
|
+
const asyncEmptyCopy = asyncEnabled && (asyncStatus === "empty" || (asyncStatus === "idle" && dropdownOptions.length === 0))
|
|
441
|
+
? noOptionsText || "No results found"
|
|
442
|
+
: "";
|
|
443
|
+
|
|
444
|
+
// A keystroke clears the highlight. Focus the first result once the next list arrives,
|
|
445
|
+
// including options the application loads itself.
|
|
446
|
+
const asyncResultOptions = loadOptions ? loadedOptions : options;
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
if (asyncEnabled && !isDropDownClosed) {
|
|
449
|
+
setFocusedOptionIndex(filteredOptions.length ? 0 : -1);
|
|
450
|
+
}
|
|
451
|
+
}, [asyncResultOptions]);
|
|
344
452
|
|
|
345
453
|
// For keyboard accessibility: Set focus within dropdown to selected item if it exists
|
|
346
454
|
useEffect(() => {
|
|
347
455
|
if (!isDropDownClosed) {
|
|
348
456
|
let newIndex = 0;
|
|
349
457
|
if (selected && !Array.isArray(selected) && selected.label) {
|
|
350
|
-
const selectedIndex = filteredOptions.findIndex((option: GenericObject) => option
|
|
458
|
+
const selectedIndex = filteredOptions.findIndex((option: GenericObject) => isSameOption(option, selected));
|
|
351
459
|
if (selectedIndex >= 0) {
|
|
352
460
|
newIndex = selectedIndex;
|
|
353
461
|
}
|
|
@@ -412,38 +520,70 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
412
520
|
]);
|
|
413
521
|
|
|
414
522
|
|
|
415
|
-
const
|
|
523
|
+
const handleSelectionChange = (value: any) => {
|
|
524
|
+
onSelect && onSelect(value);
|
|
525
|
+
onChange && onChange({ target: { name, value } });
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, replaceSelection = false) => {
|
|
416
529
|
if (disabled) return;
|
|
530
|
+
if (asyncEnabled) {
|
|
531
|
+
searchAsync(e.target.value);
|
|
532
|
+
setFocusedOptionIndex(-1);
|
|
533
|
+
}
|
|
534
|
+
if ((replaceSelection || ((asyncEnabled || onInputChange) && !e.target.value)) && autocomplete && !multiSelect && selectedArray.length) {
|
|
535
|
+
preserveQueryOnControlledClear.current = isControlled && !!e.target.value;
|
|
536
|
+
setSelected({});
|
|
537
|
+
handleSelectionChange(null);
|
|
538
|
+
}
|
|
417
539
|
setFilterItem(e.target.value);
|
|
540
|
+
onInputChange?.(e.target.value, { reason: "input" });
|
|
418
541
|
setIsDropDownClosed(false);
|
|
419
542
|
};
|
|
420
543
|
|
|
421
|
-
const handleSelectionChange = (value: any) => {
|
|
422
|
-
onSelect && onSelect(value);
|
|
423
|
-
onChange && onChange({ target: { name, value } });
|
|
424
|
-
};
|
|
425
544
|
|
|
426
545
|
const handleOptionClick = (clickedItem: GenericObject) => {
|
|
427
546
|
if (disabled) return;
|
|
547
|
+
preserveQueryOnControlledClear.current = false;
|
|
548
|
+
// Single autocomplete keeps the search results so reopening can match the selected label.
|
|
549
|
+
// Other selections still drop back to the initial list.
|
|
550
|
+
if (asyncEnabled) {
|
|
551
|
+
if (!multiSelect && autocomplete) cancelAsync();
|
|
552
|
+
else clearAsync();
|
|
553
|
+
}
|
|
428
554
|
const shouldCloseOnClick = closeOnClick === "any" || closeOnClick === "inside";
|
|
429
555
|
|
|
430
556
|
if (multiSelect) {
|
|
557
|
+
if (isControlled || getOptionValue) {
|
|
558
|
+
const exists = selectedOptionIds
|
|
559
|
+
? selectedOptionIds.has(String(getOptionValue(clickedItem)))
|
|
560
|
+
: selectedArray.some((option) => isSameOption(option, clickedItem, "value"));
|
|
561
|
+
const next = exists
|
|
562
|
+
? selectedArray.filter((option) => !isSameOption(option, clickedItem, "value"))
|
|
563
|
+
: [...selectedArray, clickedItem];
|
|
564
|
+
setSelected(next);
|
|
565
|
+
handleSelectionChange(next);
|
|
566
|
+
} else {
|
|
431
567
|
setSelected((prev) => {
|
|
432
568
|
const list = prev as GenericObject[];
|
|
433
|
-
const exists = list.find((option) => option
|
|
569
|
+
const exists = list.find((option) => isSameOption(option, clickedItem, "value"));
|
|
434
570
|
const next = exists
|
|
435
|
-
? list.filter((option) => option
|
|
571
|
+
? list.filter((option) => !isSameOption(option, clickedItem, "value"))
|
|
436
572
|
: [...list, clickedItem];
|
|
437
573
|
handleSelectionChange(next);
|
|
438
574
|
return next;
|
|
439
575
|
});
|
|
576
|
+
}
|
|
440
577
|
setFilterItem("");
|
|
441
578
|
if (shouldCloseOnClick) {
|
|
442
579
|
setIsDropDownClosed(true);
|
|
443
580
|
}
|
|
444
581
|
} else {
|
|
445
582
|
setSelected(clickedItem);
|
|
446
|
-
|
|
583
|
+
// Async autocomplete shows the selection in the input, as controlled mode and Rails do
|
|
584
|
+
setFilterItem(isControlled && autocomplete
|
|
585
|
+
? (selected as GenericObject)?.label || ""
|
|
586
|
+
: asyncEnabled && autocomplete ? clickedItem.label || "" : "");
|
|
447
587
|
if (shouldCloseOnClick) {
|
|
448
588
|
setIsDropDownClosed(true);
|
|
449
589
|
}
|
|
@@ -455,12 +595,12 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
455
595
|
|
|
456
596
|
if (controlsStartId) {
|
|
457
597
|
const startPicker = (document.querySelector(`#${controlsStartId}`) as HTMLElement & { _flatpickr?: any })?._flatpickr;
|
|
458
|
-
startPicker
|
|
598
|
+
setQuickPickDateOnPicker(startPicker, start, "start");
|
|
459
599
|
}
|
|
460
600
|
|
|
461
601
|
if (controlsEndId) {
|
|
462
602
|
const endPicker = (document.querySelector(`#${controlsEndId}`) as HTMLElement & { _flatpickr?: any })?._flatpickr;
|
|
463
|
-
endPicker
|
|
603
|
+
setQuickPickDateOnPicker(endPicker, end, "end");
|
|
464
604
|
}
|
|
465
605
|
}
|
|
466
606
|
}
|
|
@@ -474,6 +614,10 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
474
614
|
|
|
475
615
|
const handleBackspace = () => {
|
|
476
616
|
if (disabled) return;
|
|
617
|
+
preserveQueryOnControlledClear.current = false;
|
|
618
|
+
if (asyncEnabled) clearAsync();
|
|
619
|
+
onInputChange?.("", { reason: "clear" });
|
|
620
|
+
if ((onInputChange || asyncEnabled) && multiSelect) setFilterItem("");
|
|
477
621
|
if (multiSelect) {
|
|
478
622
|
setSelected([]);
|
|
479
623
|
handleSelectionChange([]);
|
|
@@ -481,7 +625,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
481
625
|
setSelected({});
|
|
482
626
|
handleSelectionChange(null);
|
|
483
627
|
setFocusedOptionIndex(-1);
|
|
484
|
-
setFilterItem("");
|
|
628
|
+
setFilterItem(isControlled && autocomplete && !multiSelect ? (selected as GenericObject)?.label || "" : "");
|
|
485
629
|
|
|
486
630
|
// Clear linked DatePickers as well if this is a quickpick variant with controls
|
|
487
631
|
if (variant === "quickpick") {
|
|
@@ -511,6 +655,9 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
511
655
|
// Create an internal ref object that holds the imperative handle methods
|
|
512
656
|
const imperativeRef = useRef({
|
|
513
657
|
clearSelected: () => {
|
|
658
|
+
preserveQueryOnControlledClear.current = false;
|
|
659
|
+
if (asyncEnabled) clearAsync();
|
|
660
|
+
onInputChange?.("", { reason: "clear" });
|
|
514
661
|
if (multiSelect) {
|
|
515
662
|
setSelected([]);
|
|
516
663
|
handleSelectionChange([]);
|
|
@@ -518,7 +665,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
518
665
|
setSelected({});
|
|
519
666
|
handleSelectionChange(null);
|
|
520
667
|
}
|
|
521
|
-
setFilterItem("");
|
|
668
|
+
setFilterItem(isControlled && autocomplete && !multiSelect ? (selected as GenericObject)?.label || "" : "");
|
|
522
669
|
setIsDropDownClosed(true);
|
|
523
670
|
},
|
|
524
671
|
});
|
|
@@ -528,6 +675,9 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
528
675
|
useEffect(() => {
|
|
529
676
|
imperativeRef.current = {
|
|
530
677
|
clearSelected: () => {
|
|
678
|
+
preserveQueryOnControlledClear.current = false;
|
|
679
|
+
if (asyncEnabled) clearAsync();
|
|
680
|
+
onInputChange?.("", { reason: "clear" });
|
|
531
681
|
if (multiSelect) {
|
|
532
682
|
setSelected([]);
|
|
533
683
|
handleSelectionChange([]);
|
|
@@ -535,11 +685,11 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
535
685
|
setSelected({});
|
|
536
686
|
handleSelectionChange(null);
|
|
537
687
|
}
|
|
538
|
-
setFilterItem("");
|
|
688
|
+
setFilterItem(isControlled && autocomplete && !multiSelect ? (selected as GenericObject)?.label || "" : "");
|
|
539
689
|
setIsDropDownClosed(true);
|
|
540
690
|
},
|
|
541
691
|
};
|
|
542
|
-
}, [multiSelect, handleSelectionChange, setSelected, setFilterItem, setIsDropDownClosed]);
|
|
692
|
+
}, [multiSelect, handleSelectionChange, setSelected, setFilterItem, setIsDropDownClosed, asyncEnabled, clearAsync, onInputChange]);
|
|
543
693
|
|
|
544
694
|
useImperativeHandle(ref, () => imperativeRef.current);
|
|
545
695
|
|
|
@@ -562,12 +712,12 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
562
712
|
setTimeout(() => {
|
|
563
713
|
if (controlsStartId) {
|
|
564
714
|
const startPicker = (document.querySelector(`#${controlsStartId}`) as HTMLElement & { _flatpickr?: any })?._flatpickr;
|
|
565
|
-
startPicker
|
|
715
|
+
setQuickPickDateOnPicker(startPicker, start, "start");
|
|
566
716
|
}
|
|
567
717
|
|
|
568
718
|
if (controlsEndId) {
|
|
569
719
|
const endPicker = (document.querySelector(`#${controlsEndId}`) as HTMLElement & { _flatpickr?: any })?._flatpickr;
|
|
570
|
-
endPicker
|
|
720
|
+
setQuickPickDateOnPicker(endPicker, end, "end");
|
|
571
721
|
}
|
|
572
722
|
}, 0);
|
|
573
723
|
}
|
|
@@ -579,6 +729,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
579
729
|
{...dataProps}
|
|
580
730
|
{...htmlProps}
|
|
581
731
|
{...(filterResetDefaultSerialized ? { "data-default-value": filterResetDefaultSerialized } : {})}
|
|
732
|
+
aria-busy={asyncEnabled ? (loading || asyncStatus === "loading") : undefined}
|
|
582
733
|
className={classes}
|
|
583
734
|
id={id}
|
|
584
735
|
ref={outerDivRef}
|
|
@@ -586,6 +737,15 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
586
737
|
>
|
|
587
738
|
<DropdownContext.Provider
|
|
588
739
|
value={{
|
|
740
|
+
asyncEnabled,
|
|
741
|
+
asyncStatus: asyncEnabled ? (loading || asyncStatus === "loading" ? "Loading…" : asyncStatus === "error" ? "Unable to load options" : asyncEmptyCopy) : "",
|
|
742
|
+
renderOption,
|
|
743
|
+
optionKey,
|
|
744
|
+
isControlled,
|
|
745
|
+
onInputChange,
|
|
746
|
+
isSameOption,
|
|
747
|
+
selectedOptionIds,
|
|
748
|
+
getOptionValue,
|
|
589
749
|
activeStyle,
|
|
590
750
|
autocomplete,
|
|
591
751
|
blankSelection,
|
|
@@ -613,6 +773,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
613
773
|
portalHost,
|
|
614
774
|
selectId,
|
|
615
775
|
multiSelect,
|
|
776
|
+
noOptionsText,
|
|
616
777
|
onSelect,
|
|
617
778
|
optionsWithBlankSelection,
|
|
618
779
|
selected,
|
|
@@ -682,7 +843,7 @@ let Dropdown = (props: DropdownProps, ref: any): React.ReactElement | null => {
|
|
|
682
843
|
<DropdownContainer constrainHeight={constrainHeight}>
|
|
683
844
|
{optionsWithBlankSelection &&
|
|
684
845
|
optionsWithBlankSelection?.map((option: GenericObject) => (
|
|
685
|
-
<DropdownOption key={option
|
|
846
|
+
<DropdownOption key={optionKey(option)}
|
|
686
847
|
option={option}
|
|
687
848
|
/>
|
|
688
849
|
))}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<%= pb_rails("dropdown", props: {
|
|
2
|
+
id: "async-rich-user-dropdown",
|
|
3
|
+
label: "User",
|
|
4
|
+
name: "async_rich_user",
|
|
5
|
+
placeholder: "Try Emily or Michael",
|
|
6
|
+
autocomplete: true,
|
|
7
|
+
async: true,
|
|
8
|
+
active_style: { background_color: "bg_light", font_color: "text_lt_default" },
|
|
9
|
+
options: [],
|
|
10
|
+
}) %>
|
|
11
|
+
|
|
12
|
+
<template id="async-user-option-template">
|
|
13
|
+
<%= pb_rails("flex", props: { orientation: "column", align: "start" }) do %>
|
|
14
|
+
<%= pb_rails("body", props: { text: "Name", data: { async_user_name: true } }) %>
|
|
15
|
+
<%= pb_rails("detail", props: { text: "Title and Department", data: { async_user_detail: true } }) %>
|
|
16
|
+
<% end %>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
(() => {
|
|
21
|
+
const dropdown = document.getElementById("async-rich-user-dropdown");
|
|
22
|
+
const template = document.getElementById("async-user-option-template");
|
|
23
|
+
if (!dropdown || !template || dropdown.dataset.asyncExampleBound) return;
|
|
24
|
+
dropdown.dataset.asyncExampleBound = "true";
|
|
25
|
+
|
|
26
|
+
dropdown.addEventListener("pb:dropdown:search", (event) => {
|
|
27
|
+
const { searchingFor, setResults, setError } = event.detail;
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
q: searchingFor,
|
|
30
|
+
limit: "10",
|
|
31
|
+
select: "firstName,lastName,company",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
fetch(`https://dummyjson.com/users/search?${params}`, { credentials: "omit" })
|
|
35
|
+
.then((response) => {
|
|
36
|
+
if (!response.ok) throw new Error("User search failed");
|
|
37
|
+
return response.json();
|
|
38
|
+
})
|
|
39
|
+
.then(({ users }) => {
|
|
40
|
+
setResults(users.map((user) => {
|
|
41
|
+
const option = {
|
|
42
|
+
id: user.id,
|
|
43
|
+
label: `${user.firstName} ${user.lastName}`,
|
|
44
|
+
value: user.id,
|
|
45
|
+
title: user.company?.title || "",
|
|
46
|
+
department: user.company?.department || "",
|
|
47
|
+
};
|
|
48
|
+
const content = template.content.cloneNode(true);
|
|
49
|
+
content.querySelector("[data-async-user-name]").textContent = option.label;
|
|
50
|
+
content.querySelector("[data-async-user-detail]").textContent =
|
|
51
|
+
[option.title, option.department].filter(Boolean).join(" · ");
|
|
52
|
+
return { option, content };
|
|
53
|
+
}));
|
|
54
|
+
})
|
|
55
|
+
.catch(() => setError());
|
|
56
|
+
});
|
|
57
|
+
})();
|
|
58
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
For custom Rails result rows, pass `{ option, content }` entries to the async search event's `setResults` callback. `active_style: { background_color: "bg_light", font_color: "text_lt_default" }` is recommended so the selected row uses a light background and default text color. Plain option objects and rich entries can appear in the same result array.
|
|
2
|
+
|
|
3
|
+
- `option` is the ordinary serializable Dropdown option object: `id`, `label`, `value`, and any custom data. `disabled: true` prevents selection.
|
|
4
|
+
- `content` is an `Element` or `DocumentFragment`, usually cloned from a Rails-rendered `<template>`. Dropdown clones it into its standard option wrapper; it does not consume or move your original nodes. Populate API text with `textContent`. HTML strings are not a rich-content input.
|
|
5
|
+
|
|
6
|
+
The explicit `option.label` is used in the selected input, even when the row contains additional text. Content is not serialized into option data or the existing `pb:dropdown:selected` payload. Nested content supports click selection and keyboard navigation, including when the menu is portaled.
|
|
7
|
+
|
|
8
|
+
Provide display content only: do not include another Dropdown option wrapper, interactive controls, or named form inputs in suggestions. Keep submitted fields outside the result content and update them from selection data. Cloning preserves markup and data attributes but does not copy JavaScript event listeners attached to the original nodes.
|
|
9
|
+
|
|
10
|
+
The example searches [DummyJSON’s public sample users API](https://dummyjson.com/docs/users) without credentials or an API key. Try `Emily` or `Michael`. It renders Rails Body and Detail kits for the name, job title, and department, retaining title and department in the selected option data. The same pattern works with a User kit or other custom display content.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Dropdown from '../_dropdown'
|
|
3
|
+
|
|
4
|
+
let searchTimer
|
|
5
|
+
let searchController
|
|
6
|
+
|
|
7
|
+
const loadUsers = (term) => new Promise((resolve, reject) => {
|
|
8
|
+
clearTimeout(searchTimer)
|
|
9
|
+
searchController?.abort()
|
|
10
|
+
const controller = searchController = new AbortController()
|
|
11
|
+
searchTimer = setTimeout(() => {
|
|
12
|
+
const params = new URLSearchParams({ q: term, limit: '10', select: 'firstName,lastName' })
|
|
13
|
+
fetch(`https://dummyjson.com/users/search?${params}`, { signal: controller.signal, credentials: 'omit' })
|
|
14
|
+
.then((response) => {
|
|
15
|
+
if (!response.ok) throw new Error('Search failed')
|
|
16
|
+
return response.json()
|
|
17
|
+
})
|
|
18
|
+
.then(({ users }) => resolve(users.map((user) => ({
|
|
19
|
+
...user,
|
|
20
|
+
label: `${user.firstName} ${user.lastName}`,
|
|
21
|
+
value: user.id,
|
|
22
|
+
}))))
|
|
23
|
+
.catch((error) => {
|
|
24
|
+
if (error.name !== 'AbortError') reject(error)
|
|
25
|
+
})
|
|
26
|
+
}, 250)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const DropdownAsyncInitialOptions = () => (
|
|
30
|
+
<Dropdown
|
|
31
|
+
activeStyle={{
|
|
32
|
+
backgroundColor: "bg_light",
|
|
33
|
+
fontColor: "text_lt_default",
|
|
34
|
+
}}
|
|
35
|
+
async
|
|
36
|
+
autocomplete
|
|
37
|
+
cacheOptions
|
|
38
|
+
defaultOptions
|
|
39
|
+
label="User"
|
|
40
|
+
loadOptions={loadUsers}
|
|
41
|
+
placeholder="Try Emily or Michael"
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
export default DropdownAsyncInitialOptions
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Initial results and caching apply only to `async` Dropdowns with `loadOptions`. `activeStyle={{ backgroundColor: "bg_light", fontColor: "text_lt_default" }}` is recommended so the selected row uses a light background and default text color.
|
|
2
|
+
|
|
3
|
+
- `defaultOptions={true}` calls `loadOptions("")` when enabled without opening the menu.
|
|
4
|
+
- `defaultOptions={options}` supplies an initial result list without making a request. Use `options` without `loadOptions` for externally managed results.
|
|
5
|
+
- `cacheOptions` reuses successful results for exact query strings, including empty results. Each Dropdown keeps at most 100 queries, evicting the oldest stored query. Errors are not cached, and caches are not shared between Dropdowns.
|
|
6
|
+
|
|
7
|
+
Both props default to `false`. When a search scope changes, use React's `key` to remount the scoped Dropdown, or manage results externally. Changing an inline loader function alone does not invalidate cached results.
|
|
8
|
+
|
|
9
|
+
Clearing removes displayed results and invalidates pending work, but retains cached results for subsequent searches. Emptying the query does not automatically restore the initial list.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Dropdown from '../_dropdown'
|
|
3
|
+
|
|
4
|
+
let searchTimer
|
|
5
|
+
let searchController
|
|
6
|
+
|
|
7
|
+
const loadUsers = (term) => new Promise((resolve, reject) => {
|
|
8
|
+
clearTimeout(searchTimer)
|
|
9
|
+
searchController?.abort()
|
|
10
|
+
const controller = searchController = new AbortController()
|
|
11
|
+
searchTimer = setTimeout(() => {
|
|
12
|
+
const params = new URLSearchParams({ q: term, limit: '10', select: 'firstName,lastName' })
|
|
13
|
+
fetch(`https://dummyjson.com/users/search?${params}`, { signal: controller.signal, credentials: 'omit' })
|
|
14
|
+
.then((response) => {
|
|
15
|
+
if (!response.ok) throw new Error('Search failed')
|
|
16
|
+
return response.json()
|
|
17
|
+
})
|
|
18
|
+
.then(({ users }) => resolve(users.map((user) => ({
|
|
19
|
+
...user,
|
|
20
|
+
label: `${user.firstName} ${user.lastName}`,
|
|
21
|
+
value: user.id,
|
|
22
|
+
}))))
|
|
23
|
+
.catch((error) => {
|
|
24
|
+
if (error.name !== 'AbortError') reject(error)
|
|
25
|
+
})
|
|
26
|
+
}, 250)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const DropdownAsyncLoading = () => (
|
|
30
|
+
<Dropdown
|
|
31
|
+
activeStyle={{
|
|
32
|
+
backgroundColor: "bg_light",
|
|
33
|
+
fontColor: "text_lt_default",
|
|
34
|
+
}}
|
|
35
|
+
async
|
|
36
|
+
autocomplete
|
|
37
|
+
label="User"
|
|
38
|
+
loadOptions={loadUsers}
|
|
39
|
+
placeholder="Try Emily or Michael"
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
export default DropdownAsyncLoading
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Use `async` with `autocomplete` or `Dropdown.Container searchbar` to search remote options. `activeStyle={{ backgroundColor: "bg_light", fontColor: "text_lt_default" }}` is recommended so the selected row uses a light background and default text color. This example queries DummyJSON's public sample users API; the consuming application owns the request and result mapping.
|
|
2
|
+
|
|
3
|
+
`loadOptions(term, callback)` may call `callback(options)` or return a promise resolving to options. Map results to the existing Dropdown shape: `id`, `label`, and `value`, plus any custom fields. Remote matches are displayed without additional local label filtering. When a search returns nothing, earlier results whose labels contain the query stay visible, including text typed after a space. After a single-select autocomplete selection, opening the menu again shows only results whose labels contain the selected label now in the input. With `multiSelect`, each pick becomes a Form Pill, the query clears, and that row stays hidden. See Async Multi Select. Results are matched by `value`, falling back to `id` and then `label`, so results with the same label stay distinct.
|
|
4
|
+
|
|
5
|
+
Dropdown calls the loader for nonempty queries. This example waits 250ms after the last keystroke and aborts the previous request, the same pause Rails uses and the external search example uses. Keep debounce and minimum query length in the application's loader or input handler, as Nitro's existing Typeahead adapters do. No request runs on mount unless `defaultOptions={true}` is supplied.
|
|
6
|
+
|
|
7
|
+
Loading, empty, and error states are built in. Opening the Dropdown before any results are available shows the empty message. Thrown errors, rejected promises, invalid results, and requests exceeding 15 seconds show the error state. Callback loaders should invoke their callback once when complete.
|
|
8
|
+
|
|
9
|
+
New searches, clearing, dismissal, disabling, and unmounting invalidate pending results. Dismissing an unfinished search restores the previous results. A search that already settled as an error or with no results keeps that message when the menu closes, and opening it again does not start another search. Stale responses cannot replace the current results. Application code owns network cancellation. Async mode does not apply to Quick Pick.
|