playbook_ui 18.2.0 → 18.3.0.pre.alpha.forwardrefwarnings20879

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +1 -1
  3. data/app/pb_kits/playbook/pb_avatar/_avatar.tsx +3 -1
  4. data/app/pb_kits/playbook/pb_avatar/kit.schema.json +1 -0
  5. data/app/pb_kits/playbook/pb_collapsible/collapsible.test.js +1 -2
  6. data/app/pb_kits/playbook/pb_contact/_contact.tsx +4 -1
  7. data/app/pb_kits/playbook/pb_contact/kit.schema.json +1 -0
  8. data/app/pb_kits/playbook/pb_date/_date.tsx +3 -1
  9. data/app/pb_kits/playbook/pb_date/date.test.js +1 -1
  10. data/app/pb_kits/playbook/pb_date/kit.schema.json +1 -0
  11. data/app/pb_kits/playbook/pb_date_picker/date_picker.test.js +4 -0
  12. data/app/pb_kits/playbook/pb_date_range_inline/_date_range_inline.tsx +3 -1
  13. data/app/pb_kits/playbook/pb_date_range_inline/kit.schema.json +1 -0
  14. data/app/pb_kits/playbook/pb_date_range_stacked/_date_range_stacked.tsx +3 -1
  15. data/app/pb_kits/playbook/pb_date_range_stacked/kit.schema.json +1 -0
  16. data/app/pb_kits/playbook/pb_date_stacked/_date_stacked.tsx +4 -1
  17. data/app/pb_kits/playbook/pb_date_stacked/date_stacked.test.js +1 -0
  18. data/app/pb_kits/playbook/pb_date_stacked/kit.schema.json +1 -0
  19. data/app/pb_kits/playbook/pb_date_time/_date_time.tsx +3 -1
  20. data/app/pb_kits/playbook/pb_date_time/dateTime.test.js +1 -0
  21. data/app/pb_kits/playbook/pb_date_time/kit.schema.json +1 -0
  22. data/app/pb_kits/playbook/pb_date_time_stacked/_date_time_stacked.tsx +2 -1
  23. data/app/pb_kits/playbook/pb_date_time_stacked/kit.schema.json +1 -0
  24. data/app/pb_kits/playbook/pb_date_year_stacked/_date_year_stacked.tsx +3 -1
  25. data/app/pb_kits/playbook/pb_date_year_stacked/date_year_stacked.test.js +1 -0
  26. data/app/pb_kits/playbook/pb_date_year_stacked/kit.schema.json +1 -0
  27. data/app/pb_kits/playbook/pb_draggable/utilities/touchDrag.ts +10 -0
  28. data/app/pb_kits/playbook/pb_dropdown/_dropdown.scss +112 -0
  29. data/app/pb_kits/playbook/pb_dropdown/_dropdown.tsx +121 -15
  30. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_draggable_pills.html.erb +51 -0
  31. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_draggable_pills.jsx +62 -0
  32. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_draggable_pills_rails.md +9 -0
  33. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_draggable_pills_react.md +9 -0
  34. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_form.jsx +50 -0
  35. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_form_react.md +9 -0
  36. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_option_creation.html.erb +16 -0
  37. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_option_creation.jsx +27 -0
  38. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_option_creation_rails.md +9 -0
  39. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_option_creation_react.md +9 -0
  40. data/app/pb_kits/playbook/pb_dropdown/docs/_playground.json +33 -2
  41. data/app/pb_kits/playbook/pb_dropdown/docs/_playground.overrides.json +13 -2
  42. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +5 -0
  43. data/app/pb_kits/playbook/pb_dropdown/docs/index.js +4 -1
  44. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +2 -2
  45. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +24 -1
  46. data/app/pb_kits/playbook/pb_dropdown/dropdown.test.jsx +235 -1
  47. data/app/pb_kits/playbook/pb_dropdown/dropdown_creation.test.jsx +414 -0
  48. data/app/pb_kits/playbook/pb_dropdown/dropdown_form.test.jsx +173 -0
  49. data/app/pb_kits/playbook/pb_dropdown/dropdown_index.test.js +545 -0
  50. data/app/pb_kits/playbook/pb_dropdown/hooks/useHandleOnKeydown.tsx +22 -5
  51. data/app/pb_kits/playbook/pb_dropdown/index.js +260 -37
  52. data/app/pb_kits/playbook/pb_dropdown/keyboard_accessibility.js +3 -1
  53. data/app/pb_kits/playbook/pb_dropdown/kit.schema.json +42 -2
  54. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownContainer.tsx +43 -25
  55. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownFormFields.tsx +83 -0
  56. data/app/pb_kits/playbook/pb_dropdown/subcomponents/DropdownOption.tsx +13 -5
  57. data/app/pb_kits/playbook/pb_dropdown/subcomponents/MultiSelectTriggerDisplay.tsx +336 -31
  58. data/app/pb_kits/playbook/pb_dropdown/utilities/optionCreation.ts +21 -0
  59. data/app/pb_kits/playbook/pb_dropdown/utilities/pillReorder.js +281 -0
  60. data/app/pb_kits/playbook/pb_dropdown/utilities/quickpickDateSync.test.ts +24 -0
  61. data/app/pb_kits/playbook/pb_dropdown/utilities/usePillDropIndicator.ts +144 -0
  62. data/app/pb_kits/playbook/pb_filter/Filter/SortMenu.tsx +8 -1
  63. data/app/pb_kits/playbook/pb_filter/docs/_filter_one_click.html.erb +38 -0
  64. data/app/pb_kits/playbook/pb_filter/docs/_filter_one_click.jsx +69 -0
  65. data/app/pb_kits/playbook/pb_filter/docs/_filter_one_click.md +1 -0
  66. data/app/pb_kits/playbook/pb_filter/docs/example.yml +2 -0
  67. data/app/pb_kits/playbook/pb_filter/docs/index.js +2 -1
  68. data/app/pb_kits/playbook/pb_filter/filter.html.erb +4 -4
  69. data/app/pb_kits/playbook/pb_filter/filter.rb +28 -0
  70. data/app/pb_kits/playbook/pb_filter/filter.test.js +41 -0
  71. data/app/pb_kits/playbook/pb_fixed_confirmation_toast/fixed_confirmation_toast.test.js +1 -2
  72. data/app/pb_kits/playbook/pb_form/docs/_description_react.md +3 -0
  73. data/app/pb_kits/playbook/pb_form/docs/_form_default_values.jsx +898 -0
  74. data/app/pb_kits/playbook/pb_form/docs/_form_default_values_react.md +3 -0
  75. data/app/pb_kits/playbook/pb_form/docs/_form_defaults.jsx +879 -0
  76. data/app/pb_kits/playbook/pb_form/docs/_form_defaults_react.md +29 -0
  77. data/app/pb_kits/playbook/pb_form/docs/_form_loading.jsx +75 -0
  78. data/app/pb_kits/playbook/pb_form/docs/{_form_loading.md → _form_loading_rails.md} +1 -0
  79. data/app/pb_kits/playbook/pb_form/docs/_form_loading_react.md +1 -0
  80. data/app/pb_kits/playbook/pb_form/docs/_form_model_values.jsx +147 -0
  81. data/app/pb_kits/playbook/pb_form/docs/_form_model_values_react.md +16 -0
  82. data/app/pb_kits/playbook/pb_form/docs/_form_multi_column.jsx +164 -0
  83. data/app/pb_kits/playbook/pb_form/docs/_form_multi_column_react.md +3 -0
  84. data/app/pb_kits/playbook/pb_form/docs/_form_validation.jsx +1057 -0
  85. data/app/pb_kits/playbook/pb_form/docs/_form_validation_react.md +26 -0
  86. data/app/pb_kits/playbook/pb_form/docs/example.yml +7 -0
  87. data/app/pb_kits/playbook/pb_form/pb_form_validation.js +1 -0
  88. data/app/pb_kits/playbook/pb_form_group/_form_group.scss +1 -4
  89. data/app/pb_kits/playbook/pb_full_screen/_full_screen.scss +6 -0
  90. data/app/pb_kits/playbook/pb_full_screen/_full_screen.tsx +11 -2
  91. data/app/pb_kits/playbook/pb_full_screen/docs/_full_screen_advanced_table_sticky_header.jsx +73 -0
  92. data/app/pb_kits/playbook/pb_full_screen/docs/_full_screen_advanced_table_sticky_header.md +1 -0
  93. data/app/pb_kits/playbook/pb_full_screen/docs/example.yml +1 -0
  94. data/app/pb_kits/playbook/pb_full_screen/docs/index.js +1 -0
  95. data/app/pb_kits/playbook/pb_full_screen/full_screen.test.jsx +4 -2
  96. data/app/pb_kits/playbook/pb_image/_image.tsx +3 -1
  97. data/app/pb_kits/playbook/pb_image/kit.schema.json +1 -0
  98. data/app/pb_kits/playbook/pb_label_pill/_label_pill.tsx +3 -1
  99. data/app/pb_kits/playbook/pb_label_pill/kit.schema.json +1 -0
  100. data/app/pb_kits/playbook/pb_legend/_legend.tsx +3 -1
  101. data/app/pb_kits/playbook/pb_legend/kit.schema.json +1 -0
  102. data/app/pb_kits/playbook/pb_list/_list_item.tsx +6 -3
  103. data/app/pb_kits/playbook/pb_map/docs/_map_default.md +22 -3
  104. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +4 -1
  105. data/app/pb_kits/playbook/pb_online_status/_online_status.tsx +3 -1
  106. data/app/pb_kits/playbook/pb_online_status/kit.schema.json +1 -0
  107. data/app/pb_kits/playbook/pb_pb_bar_graph/pbbargraph.test.jsx +1 -2
  108. data/app/pb_kits/playbook/pb_pb_circle_chart/pb_circle_chart.test.jsx +1 -2
  109. data/app/pb_kits/playbook/pb_pb_gauge_chart/pb_gauge_chart.test.jsx +1 -2
  110. data/app/pb_kits/playbook/pb_pb_line_graph/pb_line_graph.test.jsx +1 -2
  111. data/app/pb_kits/playbook/pb_person/_person.tsx +3 -1
  112. data/app/pb_kits/playbook/pb_person/kit.schema.json +1 -0
  113. data/app/pb_kits/playbook/pb_pill/_pill.tsx +3 -1
  114. data/app/pb_kits/playbook/pb_pill/kit.schema.json +1 -0
  115. data/app/pb_kits/playbook/pb_progress_step/_progress_step_item.tsx +3 -1
  116. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.json +1 -1
  117. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.overrides.json +1 -1
  118. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_required_indicator.md +2 -2
  119. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_default.md +3 -4
  120. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_extensions.md +1 -1
  121. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.md +1 -1
  122. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_required_indicator.html.erb +9 -0
  123. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_required_indicator.md +3 -0
  124. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +1 -0
  125. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +3 -7
  126. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_rails.js +26 -21
  127. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_rails.test.js +85 -1
  128. data/app/pb_kits/playbook/pb_selectable_card_icon/_selectable_card_icon.scss +0 -9
  129. data/app/pb_kits/playbook/pb_selectable_card_icon/docs/_selectable_card_icon_custom.jsx +1 -1
  130. data/app/pb_kits/playbook/pb_selectable_icon/_selectable_icon.scss +0 -7
  131. data/app/pb_kits/playbook/pb_source/_source.tsx +3 -1
  132. data/app/pb_kits/playbook/pb_source/kit.schema.json +1 -0
  133. data/app/pb_kits/playbook/pb_source/source.rb +1 -1
  134. data/app/pb_kits/playbook/pb_stat_change/_stat_change.tsx +3 -1
  135. data/app/pb_kits/playbook/pb_stat_change/kit.schema.json +1 -0
  136. data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +4 -1
  137. data/app/pb_kits/playbook/pb_time_range_inline/_time_range_inline.tsx +3 -1
  138. data/app/pb_kits/playbook/pb_time_range_inline/kit.schema.json +1 -0
  139. data/app/pb_kits/playbook/pb_time_stacked/_time_stacked.tsx +3 -1
  140. data/app/pb_kits/playbook/pb_time_stacked/kit.schema.json +1 -0
  141. data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -1
  142. data/app/pb_kits/playbook/pb_timestamp/_timestamp.tsx +3 -1
  143. data/app/pb_kits/playbook/pb_timestamp/kit.schema.json +1 -0
  144. data/app/pb_kits/playbook/pb_timestamp/timestamp.test.js +1 -0
  145. data/app/pb_kits/playbook/pb_typeahead/_typeahead.scss +73 -0
  146. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +285 -12
  147. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +22 -14
  148. data/app/pb_kits/playbook/pb_typeahead/components/PillRenderer.tsx +15 -11
  149. data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +29 -30
  150. data/app/pb_kits/playbook/pb_typeahead/components/usePillDropIndicator.ts +155 -0
  151. data/app/pb_kits/playbook/pb_typeahead/docs/_playground.json +5 -5
  152. data/app/pb_kits/playbook/pb_typeahead/docs/_playground.overrides.json +2 -2
  153. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.html.erb +0 -1
  154. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.jsx +0 -1
  155. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills_rails.md +3 -3
  156. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills_react.md +7 -0
  157. data/app/pb_kits/playbook/pb_typeahead/kit.schema.json +3 -3
  158. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +3 -3
  159. data/app/pb_kits/playbook/pb_user/_user.tsx +3 -1
  160. data/app/pb_kits/playbook/pb_user/kit.schema.json +1 -0
  161. data/app/pb_kits/playbook/pb_user_badge/_user_badge.tsx +3 -1
  162. data/app/pb_kits/playbook/pb_user_badge/kit.schema.json +1 -0
  163. data/app/pb_kits/playbook/pb_weekday_stacked/_weekday_stacked.tsx +3 -1
  164. data/app/pb_kits/playbook/pb_weekday_stacked/kit.schema.json +1 -0
  165. data/app/pb_kits/playbook/pb_weekday_stacked/weekday_stacked.test.jsx +1 -0
  166. data/app/pb_kits/playbook/utilities/test/globalProps/onClick.test.js +302 -0
  167. data/app/pb_kits/playbook/utilities/test-utils.js +0 -1
  168. data/dist/ai/all-schemas.json +108 -15
  169. data/dist/ai/external-dependencies.json +1 -1
  170. data/dist/ai/forms.json +2 -2
  171. data/dist/ai/index.json +3 -3
  172. data/dist/ai/kits/avatar.schema.json +1 -0
  173. data/dist/ai/kits/contact.schema.json +1 -0
  174. data/dist/ai/kits/date.schema.json +1 -0
  175. data/dist/ai/kits/date_range_inline.schema.json +1 -0
  176. data/dist/ai/kits/date_range_stacked.schema.json +1 -0
  177. data/dist/ai/kits/date_stacked.schema.json +1 -0
  178. data/dist/ai/kits/date_time.schema.json +1 -0
  179. data/dist/ai/kits/date_time_stacked.schema.json +1 -0
  180. data/dist/ai/kits/date_year_stacked.schema.json +1 -0
  181. data/dist/ai/kits/dropdown.schema.json +81 -11
  182. data/dist/ai/kits/image.schema.json +1 -0
  183. data/dist/ai/kits/label_pill.schema.json +1 -0
  184. data/dist/ai/kits/legend.schema.json +1 -0
  185. data/dist/ai/kits/online_status.schema.json +1 -0
  186. data/dist/ai/kits/person.schema.json +1 -0
  187. data/dist/ai/kits/pill.schema.json +1 -0
  188. data/dist/ai/kits/rich_text_editor.schema.json +1 -1
  189. data/dist/ai/kits/source.schema.json +1 -0
  190. data/dist/ai/kits/stat_change.schema.json +1 -0
  191. data/dist/ai/kits/time_range_inline.schema.json +1 -0
  192. data/dist/ai/kits/time_stacked.schema.json +1 -0
  193. data/dist/ai/kits/timestamp.schema.json +1 -0
  194. data/dist/ai/kits/typeahead.schema.json +3 -3
  195. data/dist/ai/kits/user.schema.json +1 -0
  196. data/dist/ai/kits/user_badge.schema.json +1 -0
  197. data/dist/ai/kits/weekday_stacked.schema.json +1 -0
  198. data/dist/ai/playgrounds/dropdown.json +1 -1
  199. data/dist/ai/playgrounds/index.json +1 -1
  200. data/dist/ai/playgrounds/rich_text_editor.json +1 -1
  201. data/dist/ai/playgrounds/typeahead.json +1 -1
  202. data/dist/chunks/_pagination-CARbC_IN.js +1 -0
  203. data/dist/chunks/{_pb_line_graph-Du7xE9vl.js → _pb_line_graph-BKwbOeqo.js} +1 -1
  204. data/dist/chunks/{_detail-Bsvl694i.js → _tooltip-UkcgcThO.js} +2 -2
  205. data/dist/chunks/_typeahead-D_li2Zx8.js +1 -0
  206. data/dist/chunks/{globalProps-B120NbSJ.js → globalProps-BivjwRew.js} +1 -1
  207. data/dist/chunks/index-4jrZjexD.js +1 -0
  208. data/dist/chunks/index-BAy61y8S.js +1 -0
  209. data/dist/chunks/index-Cb_SDipk.js +1 -0
  210. data/dist/chunks/index-D3RdsDp7.js +1 -0
  211. data/dist/chunks/index-D8PZIesy.js +1 -0
  212. data/dist/chunks/index-DIoc-aua.js +1 -0
  213. data/dist/chunks/index-qex65UMn.js +1 -0
  214. data/dist/chunks/lib-C50eCpoN.js +29 -0
  215. data/dist/chunks/{time_picker_helper-CYGg0UQ0.js → time_picker_helper-DfKvHCJu.js} +1 -1
  216. data/dist/chunks/vendor.js +2 -2
  217. data/dist/menu.yml +1 -1
  218. data/dist/playbook-rails-react-bindings.js +1 -1
  219. data/dist/playbook-rails.js +1 -1
  220. data/dist/playbook.css +1 -1
  221. data/lib/playbook/version.rb +2 -2
  222. metadata +80 -21
  223. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.md +0 -7
  224. data/dist/chunks/_table-Bf6LGyL9.js +0 -1
  225. data/dist/chunks/_typeahead-w6S5Vx4b.js +0 -1
  226. data/dist/chunks/lib-Buo6zHEw.js +0 -29
  227. /data/app/pb_kits/playbook/pb_form/docs/{_description.md → _description_rails.md} +0 -0
  228. /data/app/pb_kits/playbook/pb_form/docs/{_form_default_values.md → _form_default_values_rails.md} +0 -0
  229. /data/app/pb_kits/playbook/pb_form/docs/{_form_defaults.md → _form_defaults_rails.md} +0 -0
  230. /data/app/pb_kits/playbook/pb_form/docs/{_form_model_values.md → _form_model_values_rails.md} +0 -0
  231. /data/app/pb_kits/playbook/pb_form/docs/{_form_multi_column.md → _form_multi_column_rails.md} +0 -0
  232. /data/app/pb_kits/playbook/pb_form/docs/{_form_validation.md → _form_validation_rails.md} +0 -0
@@ -0,0 +1,83 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { GenericObject } from "../../types";
3
+
4
+ type DropdownFormFieldsProps = {
5
+ disabled: boolean;
6
+ multiSelect: boolean;
7
+ name?: string;
8
+ onInvalid: () => void;
9
+ onReset: () => void;
10
+ required: boolean;
11
+ selected: GenericObject[];
12
+ validationMessage: string;
13
+ };
14
+
15
+ const DropdownFormFields = ({
16
+ disabled,
17
+ multiSelect,
18
+ name,
19
+ onInvalid,
20
+ onReset,
21
+ required,
22
+ selected,
23
+ validationMessage,
24
+ }: DropdownFormFieldsProps): React.ReactElement => {
25
+ const validationRef = useRef<HTMLInputElement>(null);
26
+ const onResetRef = useRef(onReset);
27
+ onResetRef.current = onReset;
28
+
29
+ const hasSelection = selected.some((option) => String(option.id ?? option.value ?? "") !== "");
30
+ const needsArraySuffix = multiSelect && name && !name.endsWith("[]");
31
+ const fieldName = needsArraySuffix ? `${name}[]` : name;
32
+ const submittedOptions = hasSelection ? selected : [{}];
33
+
34
+ useEffect(() => {
35
+ validationRef.current?.setCustomValidity(required && !hasSelection ? validationMessage : "");
36
+ }, [required, hasSelection, validationMessage]);
37
+
38
+ useEffect(() => {
39
+ const form = validationRef.current?.form;
40
+ const handleReset = (event: Event) => {
41
+ // Let native controls reset first, and honor listeners that cancel the reset.
42
+ Promise.resolve().then(() => {
43
+ if (!event.defaultPrevented) onResetRef.current();
44
+ });
45
+ };
46
+ form?.addEventListener("reset", handleReset);
47
+ return () => form?.removeEventListener("reset", handleReset);
48
+ }, []);
49
+
50
+ return (
51
+ <>
52
+ {name && submittedOptions.map((option, index) => (
53
+ <input disabled={disabled}
54
+ key={index}
55
+ name={fieldName}
56
+ type="hidden"
57
+ value={option.id ?? option.value ?? ""}
58
+ />
59
+ ))}
60
+ <input aria-hidden="true"
61
+ disabled={disabled}
62
+ onChange={() => undefined}
63
+ onInvalid={(event) => {
64
+ event.preventDefault();
65
+ onInvalid();
66
+ }}
67
+ ref={validationRef}
68
+ required={required && !hasSelection}
69
+ style={{
70
+ position: "absolute",
71
+ opacity: 0,
72
+ width: 1,
73
+ height: 1,
74
+ pointerEvents: "none",
75
+ }}
76
+ tabIndex={-1}
77
+ value={hasSelection ? "selected" : ""}
78
+ />
79
+ </>
80
+ );
81
+ };
82
+
83
+ export default DropdownFormFields;
@@ -9,6 +9,7 @@ import {
9
9
  import { globalProps, GlobalProps } from "../../utilities/globalProps";
10
10
 
11
11
  import DropdownContext from "../context";
12
+ import { matchesOptionExactly } from "../utilities/optionCreation";
12
13
 
13
14
  import Body from "../../pb_body/_body";
14
15
  import ListItem from "../../pb_list/_list_item";
@@ -36,12 +37,13 @@ const DropdownOption = (props: DropdownOptionProps) => {
36
37
  data = {},
37
38
  htmlOptions = {},
38
39
  id,
39
- key,
40
40
  option,
41
41
  } = props;
42
42
 
43
43
  const {
44
44
  asyncEnabled,
45
+ creationOption,
46
+ creationEnabled,
45
47
  autocomplete,
46
48
  activeStyle,
47
49
  disabled,
@@ -70,7 +72,8 @@ const DropdownOption = (props: DropdownOptionProps) => {
70
72
  const filterText =
71
73
  selectedLabel && filterItem === selectedLabel ? "" : filterItem;
72
74
  const label = typeof option?.label === 'string' ? option.label.toLowerCase() : option?.label;
73
- return String(label).toLowerCase().includes(filterText.toLowerCase());
75
+ return String(label).toLowerCase().includes(filterText.toLowerCase()) ||
76
+ (creationEnabled && matchesOptionExactly(option, filterText));
74
77
  }
75
78
 
76
79
  // When multiSelect, then if an option is selected, remove from dropdown
@@ -84,7 +87,8 @@ const DropdownOption = (props: DropdownOptionProps) => {
84
87
  const isOptionDisabled = option?.disabled === true;
85
88
  const isDisabled = disabled || isOptionDisabled;
86
89
 
87
- if (!isItemMatchingFilter(option) || (multiSelect && isSelected)) {
90
+ const isCreationOption = option === creationOption;
91
+ if ((!isCreationOption && !isItemMatchingFilter(option)) || (multiSelect && isSelected)) {
88
92
  return null;
89
93
  }
90
94
  const isFocused =
@@ -122,7 +126,12 @@ const DropdownOption = (props: DropdownOptionProps) => {
122
126
  isDisabled && "disabled"
123
127
  );
124
128
 
125
- const content = children || (renderOption && option ? renderOption(option) : null);
129
+ let content = children || null;
130
+ if (isCreationOption) {
131
+ content = `Create "${option.label}"`;
132
+ } else if (!content && renderOption && option) {
133
+ content = renderOption(option);
134
+ }
126
135
 
127
136
  return (
128
137
  <div
@@ -132,7 +141,6 @@ const DropdownOption = (props: DropdownOptionProps) => {
132
141
  aria-disabled={isDisabled}
133
142
  className={classes}
134
143
  id={id}
135
- key={key}
136
144
  onClick={isDisabled ? undefined : () => handleOptionClick(option)}
137
145
  >
138
146
  <ListItem
@@ -1,10 +1,15 @@
1
- import React, { useContext } from "react";
2
- import FormPill from "../../pb_form_pill/_form_pill";
3
- import Flex from "../../pb_flex/_flex";
1
+ import React, { useCallback, useContext, useMemo, useRef } from "react";
4
2
  import Body from "../../pb_body/_body";
3
+ import Draggable from "../../pb_draggable/_draggable";
4
+ import { DraggableContext, DraggableProvider } from "../../pb_draggable/context";
5
+ import Flex from "../../pb_flex/_flex";
6
+ import FormPill from "../../pb_form_pill/_form_pill";
7
+ import Icon from "../../pb_icon/_icon";
8
+ import { noop } from "../../utilities/object";
5
9
  import { GenericObject } from "../../types";
6
- import DropdownContext
7
- from "../context";
10
+ import DropdownContext from "../context";
11
+ import usePillDropIndicator from "../utilities/usePillDropIndicator";
12
+
8
13
  type MultiSelectTriggerDisplayProps = {
9
14
  autocomplete?: boolean;
10
15
  selected: GenericObject[];
@@ -12,52 +17,352 @@ type MultiSelectTriggerDisplayProps = {
12
17
  dark?: boolean;
13
18
  };
14
19
 
20
+ type DropdownPillProps = {
21
+ dark?: boolean;
22
+ formPillProps?: GenericObject;
23
+ index: number;
24
+ onKeyboardReorder?: (direction: "left" | "right") => void;
25
+ onRemove: (event: React.MouseEvent | React.KeyboardEvent) => void;
26
+ option: GenericObject;
27
+ externalDragHandle?: boolean;
28
+ showPillIndex?: boolean;
29
+ totalCount: number;
30
+ };
31
+
32
+ const DropdownPill = ({
33
+ dark = false,
34
+ formPillProps,
35
+ index,
36
+ onKeyboardReorder,
37
+ onRemove,
38
+ option,
39
+ externalDragHandle = false,
40
+ showPillIndex = false,
41
+ totalCount,
42
+ }: DropdownPillProps) => {
43
+ const baseLabel = option.label || "";
44
+ const label = showPillIndex ? `${index + 1}. ${baseLabel}` : baseLabel;
45
+ const positionLabel = totalCount > 0
46
+ ? `${baseLabel}, position ${index + 1} of ${totalCount}, press Left or Right Arrow keys to reorder`
47
+ : baseLabel;
48
+ const pillIcon = !externalDragHandle && onKeyboardReorder ? "grip-dots-vertical" : "";
49
+
50
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
51
+ if (event.key === "Backspace" || event.key === "Delete") {
52
+ event.preventDefault();
53
+ event.stopPropagation();
54
+ onRemove(event);
55
+ } else if (onKeyboardReorder && (event.key === "ArrowLeft" || event.key === "ArrowRight")) {
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+ const pill = event.currentTarget;
59
+ onKeyboardReorder(event.key === "ArrowLeft" ? "left" : "right");
60
+ requestAnimationFrame(() => pill.focus());
61
+ }
62
+ };
63
+
64
+ const pill = (
65
+ <FormPill
66
+ dark={dark}
67
+ htmlOptions={{ "aria-label": positionLabel, onKeyDown: handleKeyDown }}
68
+ icon={pillIcon}
69
+ marginRight="xs"
70
+ name=""
71
+ onClick={(event) => {
72
+ event.stopPropagation();
73
+ onRemove(event);
74
+ }}
75
+ tabIndex={0}
76
+ text={label}
77
+ {...formPillProps}
78
+ />
79
+ );
80
+
81
+ if (!externalDragHandle) return pill;
82
+
83
+ return (
84
+ <span className="pb_dropdown_pill_with_handle">
85
+ <span
86
+ aria-hidden="true"
87
+ className="pb_draggable_handle pb_dropdown_external_drag_handle"
88
+ onMouseDown={(event) => {
89
+ event.stopPropagation();
90
+ }}
91
+ >
92
+ <Body color="light">
93
+ <Icon
94
+ icon="grip-dots-vertical"
95
+ verticalAlign="middle"
96
+ />
97
+ </Body>
98
+ </span>
99
+ {pill}
100
+ </span>
101
+ );
102
+ };
103
+
104
+ type DraggablePillListProps = {
105
+ dark?: boolean;
106
+ formPillProps?: GenericObject;
107
+ getItemId: (item: GenericObject) => string;
108
+ onRemove: (option: GenericObject) => void;
109
+ onReorder: (next: GenericObject[]) => void;
110
+ pendingReorderRef: React.MutableRefObject<GenericObject[] | null>;
111
+ externalDragHandle: boolean;
112
+ selected: GenericObject[];
113
+ showPillIndex: boolean;
114
+ };
115
+
116
+ const DraggablePillList = ({
117
+ dark,
118
+ formPillProps,
119
+ getItemId,
120
+ onRemove,
121
+ onReorder,
122
+ pendingReorderRef,
123
+ externalDragHandle,
124
+ selected,
125
+ showPillIndex,
126
+ }: DraggablePillListProps) => {
127
+ const { isDragging: dragId } = DraggableContext();
128
+ const dropTarget = usePillDropIndicator(
129
+ dragId,
130
+ selected,
131
+ getItemId,
132
+ pendingReorderRef,
133
+ );
134
+
135
+ const getDropLineClassName = (index: number): string | undefined => {
136
+ if (dropTarget?.index !== index) return undefined;
137
+ return dropTarget.after ? "pb_dropdown_drop_after" : "pb_dropdown_drop_before";
138
+ };
139
+
140
+ const handleKeyboardReorder = (index: number, direction: "left" | "right") => {
141
+ const newIndex = direction === "left" ? index - 1 : index + 1;
142
+ if (newIndex < 0 || newIndex >= selected.length) return;
143
+
144
+ const reordered = [...selected];
145
+ const [movedItem] = reordered.splice(index, 1);
146
+ reordered.splice(newIndex, 0, movedItem);
147
+ onReorder(reordered);
148
+ };
149
+
150
+ return (
151
+ <div
152
+ onClick={(event) => event.stopPropagation()}
153
+ onMouseDown={(event) => {
154
+ if ((event.target as HTMLElement | null)?.closest(".pb_form_pill_close")) return
155
+ event.stopPropagation()
156
+ }}
157
+ >
158
+ <Draggable.Container
159
+ className="pb_dropdown_draggable_pills"
160
+ container="dropdown-pills"
161
+ tag="div"
162
+ >
163
+ {selected.map((option, index) => (
164
+ <Draggable.Item
165
+ className={getDropLineClassName(index)}
166
+ container="dropdown-pills"
167
+ dragId={getItemId(option)}
168
+ handlePointerDrag={externalDragHandle}
169
+ key={getItemId(option)}
170
+ pointerDrag={!externalDragHandle}
171
+ tag="span"
172
+ >
173
+ <DropdownPill
174
+ dark={dark}
175
+ externalDragHandle={externalDragHandle}
176
+ formPillProps={formPillProps}
177
+ index={index}
178
+ onKeyboardReorder={(direction) => handleKeyboardReorder(index, direction)}
179
+ onRemove={() => onRemove(option)}
180
+ option={option}
181
+ showPillIndex={showPillIndex}
182
+ totalCount={selected.length}
183
+ />
184
+ </Draggable.Item>
185
+ ))}
186
+ </Draggable.Container>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ type DraggablePillsStripProps = {
192
+ dark?: boolean;
193
+ formPillProps?: GenericObject;
194
+ getItemId: (item: GenericObject) => string;
195
+ onRemove: (option: GenericObject) => void;
196
+ onReorder: (next: GenericObject[]) => void;
197
+ externalDragHandle: boolean;
198
+ selected: GenericObject[];
199
+ showPillIndex: boolean;
200
+ };
201
+
202
+ const buildSelectedPillsKey = (value: GenericObject[], getItemId: (item: GenericObject) => string): string => (
203
+ value.map((item) => `${getItemId(item)}:${item.label ?? ""}`).join("|")
204
+ );
205
+
206
+ const formPillPropsKey = (formPillProps?: GenericObject): string => (
207
+ JSON.stringify(formPillProps ?? {})
208
+ );
209
+
210
+ const DraggablePillsStrip = React.memo(function DraggablePillsStrip({
211
+ dark,
212
+ formPillProps,
213
+ getItemId,
214
+ onRemove,
215
+ onReorder,
216
+ externalDragHandle,
217
+ selected,
218
+ showPillIndex,
219
+ }: DraggablePillsStripProps) {
220
+ const pendingReorderRef = useRef<GenericObject[] | null>(null);
221
+ const selectedRef = useRef(selected);
222
+ selectedRef.current = selected;
223
+
224
+ const initialItems = useMemo(
225
+ () => selected.map((item) => ({
226
+ id: getItemId(item),
227
+ container: "dropdown-pills",
228
+ label: item.label,
229
+ value: item.value,
230
+ })),
231
+ [getItemId, selected],
232
+ );
233
+
234
+ const commitReorder = useCallback(() => {
235
+ if (!pendingReorderRef.current) return;
236
+
237
+ const currentOrder = selectedRef.current.map((item) => getItemId(item)).join(",");
238
+ const newOrder = pendingReorderRef.current.map((item) => getItemId(item)).join(",");
239
+
240
+ if (currentOrder !== newOrder) {
241
+ onReorder(pendingReorderRef.current);
242
+ }
243
+
244
+ pendingReorderRef.current = null;
245
+ }, [getItemId, onReorder]);
246
+
247
+ return (
248
+ <DraggableProvider
249
+ dropZone={{ type: "ghost" }}
250
+ initialItems={initialItems}
251
+ onDragEnd={commitReorder}
252
+ onDrop={commitReorder}
253
+ onReorder={noop}
254
+ >
255
+ <DraggablePillList
256
+ dark={dark}
257
+ externalDragHandle={externalDragHandle}
258
+ formPillProps={formPillProps}
259
+ getItemId={getItemId}
260
+ onRemove={onRemove}
261
+ onReorder={onReorder}
262
+ pendingReorderRef={pendingReorderRef}
263
+ selected={selected}
264
+ showPillIndex={showPillIndex}
265
+ />
266
+ </DraggableProvider>
267
+ );
268
+ }, (prevProps, nextProps) => (
269
+ prevProps.dark === nextProps.dark
270
+ && prevProps.externalDragHandle === nextProps.externalDragHandle
271
+ && prevProps.showPillIndex === nextProps.showPillIndex
272
+ && formPillPropsKey(prevProps.formPillProps) === formPillPropsKey(nextProps.formPillProps)
273
+ && buildSelectedPillsKey(prevProps.selected, prevProps.getItemId)
274
+ === buildSelectedPillsKey(nextProps.selected, nextProps.getItemId)
275
+ ));
276
+
15
277
  const MultiSelectTriggerDisplay = ({
16
278
  autocomplete,
17
279
  selected,
18
280
  placeholder,
19
281
  dark = false,
20
282
  }: MultiSelectTriggerDisplayProps) => {
283
+ const {
284
+ disabled,
285
+ enablePillReorder,
286
+ formPillProps,
287
+ getOptionValue,
288
+ handleSelectionChange,
289
+ isControlled,
290
+ isSameOption,
291
+ optionKey,
292
+ externalDragHandle = false,
293
+ setSelected,
294
+ showPillIndex = false,
295
+ } = useContext(DropdownContext);
21
296
 
22
- const { setSelected, handleSelectionChange, formPillProps, isControlled, isSameOption, getOptionValue } = useContext(DropdownContext);
297
+ const getItemId = useCallback((option: GenericObject) => (
298
+ optionKey ? optionKey(option) : String(
299
+ getOptionValue ? getOptionValue(option) : option.id ?? option.value ?? option.label
300
+ )
301
+ ), [getOptionValue, optionKey]);
23
302
 
24
303
  if (selected.length === 0) {
25
304
  if (autocomplete) return null;
26
305
  return (
27
- <Body
28
- color="lighter"
29
- dark={dark}
30
- text={placeholder ? placeholder : "Select..."}
31
- />
32
- )
306
+ <Body
307
+ color="lighter"
308
+ dark={dark}
309
+ text={placeholder ? placeholder : "Select..."}
310
+ />
311
+ );
33
312
  }
34
313
 
35
- const handleRemoveIconClick = (option: GenericObject) => {
36
- if (isControlled || getOptionValue) {
37
- const next = selected.filter((item) => !isSameOption(item, option));
38
- setSelected(next);
39
- handleSelectionChange(next);
40
- return;
41
- }
42
- setSelected((prev: GenericObject[]) => {
43
- const next = prev.filter((item) => !isSameOption(item, option));
314
+ const applySelection = (next: GenericObject[]) => {
315
+ if (isControlled || getOptionValue) {
316
+ setSelected(next);
317
+ handleSelectionChange(next);
318
+ return;
319
+ }
320
+ setSelected(() => {
44
321
  handleSelectionChange && handleSelectionChange(next);
45
322
  return next;
46
323
  });
47
- }
324
+ };
325
+
326
+ const handleRemoveIconClick = (option: GenericObject) => {
327
+ if (disabled) return;
328
+ applySelection(selected.filter((item) => !isSameOption(item, option)));
329
+ };
330
+
331
+ const handleReorder = (next: GenericObject[]) => {
332
+ if (disabled) return;
333
+ applySelection(next);
334
+ };
335
+
336
+ if (enablePillReorder && !disabled) {
337
+ return (
338
+ <DraggablePillsStrip
339
+ dark={dark}
340
+ externalDragHandle={Boolean(externalDragHandle)}
341
+ formPillProps={formPillProps}
342
+ getItemId={getItemId}
343
+ onRemove={handleRemoveIconClick}
344
+ onReorder={handleReorder}
345
+ selected={selected}
346
+ showPillIndex={Boolean(showPillIndex)}
347
+ />
348
+ );
349
+ }
48
350
 
49
351
  return (
50
352
  <Flex wrap>
51
353
  {selected.map((option, i) => (
52
- <FormPill
53
- dark={dark}
54
- key={getOptionValue ? getOptionValue(option) : i}
55
- marginRight="xs"
56
- onClick={(e)=>{e.stopPropagation();handleRemoveIconClick(option)}}
57
- tabIndex={0}
58
- text={option.label}
59
- {...formPillProps}
60
- />
354
+ <FormPill
355
+ dark={dark}
356
+ key={getOptionValue ? getOptionValue(option) : i}
357
+ marginRight="xs"
358
+ onClick={(e) => {
359
+ e.stopPropagation();
360
+ handleRemoveIconClick(option);
361
+ }}
362
+ tabIndex={0}
363
+ text={showPillIndex ? `${i + 1}. ${option.label}` : option.label}
364
+ {...formPillProps}
365
+ />
61
366
  ))}
62
367
  </Flex>
63
368
  );
@@ -0,0 +1,21 @@
1
+ import { GenericObject } from "../../types";
2
+
3
+ export const matchesOptionExactly = (option: GenericObject | undefined, query: string): boolean => {
4
+ const normalizedQuery = query.trim().toLowerCase();
5
+ if (!option || !normalizedQuery) return false;
6
+
7
+ const optionValues = [option.label, option.value, option.id];
8
+ return optionValues.some((value) => (
9
+ value != null && String(value).trim().toLowerCase() === normalizedQuery
10
+ ));
11
+ };
12
+
13
+ export const getCreationOption = (query: string, options: GenericObject[]): GenericObject | null => {
14
+ const text = query.trim();
15
+ if (!text) return null;
16
+
17
+ const optionExists = options.some((option) => matchesOptionExactly(option, text));
18
+ if (optionExists) return null;
19
+
20
+ return { id: text, label: text, value: text, __isNew__: true };
21
+ };