playbook_ui 15.6.0.pre.rc.4 → 15.7.0.pre.alpha.play263313229

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/_playbook.scss +1 -1
  3. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +3 -2
  4. data/app/pb_kits/playbook/pb_advanced_table/Components/TableHeaderCell.tsx +4 -0
  5. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +95 -0
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_colors_rails.html.erb +43 -0
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_colors_rails.md +1 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_control_rails.html.erb +11 -5
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_background_control_rails.md +7 -1
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background.jsx +54 -0
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background.md +9 -0
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_multi.jsx +80 -0
  13. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_column_styling_background_multi.md +3 -0
  14. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +4 -1
  15. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +3 -1
  16. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +2 -2
  17. data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +57 -0
  18. data/app/pb_kits/playbook/pb_bar_graph/_bar_graph.tsx +6 -0
  19. data/app/pb_kits/playbook/pb_card/docs/_card_header.md +1 -1
  20. data/app/pb_kits/playbook/pb_card/docs/_card_highlight.md +1 -1
  21. data/app/pb_kits/playbook/pb_circle_chart/_circle_chart.tsx +6 -0
  22. data/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap +2 -2
  23. data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleIcon.tsx +10 -8
  24. data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_icons.jsx +0 -1
  25. data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_state.jsx +0 -3
  26. data/app/pb_kits/playbook/pb_contact/_contact.tsx +51 -24
  27. data/app/pb_kits/playbook/pb_contact/contact.html.erb +53 -19
  28. data/app/pb_kits/playbook/pb_contact/contact.rb +11 -1
  29. data/app/pb_kits/playbook/pb_contact/contact.test.js +76 -0
  30. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled.html.erb +33 -0
  31. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled.jsx +46 -0
  32. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled_rails.md +2 -0
  33. data/app/pb_kits/playbook/pb_contact/docs/_contact_unstyled_react.md +2 -0
  34. data/app/pb_kits/playbook/pb_contact/docs/example.yml +2 -0
  35. data/app/pb_kits/playbook/pb_contact/docs/index.js +1 -0
  36. data/app/pb_kits/playbook/pb_date_picker/date_picker.test.js +24 -0
  37. data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +197 -7
  38. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_range_pattern_rails.html.erb +23 -14
  39. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_range_pattern_rails.md +1 -1
  40. data/app/pb_kits/playbook/pb_dialog/_dialog.tsx +2 -1
  41. data/app/pb_kits/playbook/pb_dialog/dialog.html.erb +1 -1
  42. data/app/pb_kits/playbook/pb_dialog/dialog.rb +1 -0
  43. data/app/pb_kits/playbook/pb_dialog/dialog.test.jsx +14 -0
  44. data/app/pb_kits/playbook/pb_dialog/dialog_header.html.erb +5 -4
  45. data/app/pb_kits/playbook/pb_dialog/dialog_header.rb +2 -0
  46. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.html.erb +24 -0
  47. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.jsx +60 -0
  48. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.md +3 -0
  49. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible.html.erb +71 -0
  50. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible.jsx +57 -0
  51. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible_rails.md +1 -0
  52. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_overflow_visible_react.md +1 -0
  53. data/app/pb_kits/playbook/pb_dialog/docs/example.yml +4 -0
  54. data/app/pb_kits/playbook/pb_dialog/docs/index.js +3 -1
  55. data/app/pb_kits/playbook/pb_distribution_bar/docs/_distribution_bar_custom_colors.md +1 -1
  56. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +316 -15
  57. data/app/pb_kits/playbook/pb_draggable/context/types.ts +1 -1
  58. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_default_rails.html.erb +7 -5
  59. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_default_dates.html.erb +19 -0
  60. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_rails.html.erb +12 -0
  61. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_rails.md +26 -0
  62. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_range_end_rails.html.erb +19 -0
  63. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_range_end_rails.md +1 -0
  64. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_default_rails.html.erb +30 -0
  65. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_default_rails.md +3 -0
  66. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_rails.html.erb +29 -0
  67. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_quickpick_with_date_pickers_rails.md +13 -0
  68. data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_with_custom_display_rails.html.erb +3 -1
  69. data/app/pb_kits/playbook/pb_dropdown/docs/example.yml +5 -0
  70. data/app/pb_kits/playbook/pb_dropdown/dropdown.html.erb +4 -0
  71. data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +39 -5
  72. data/app/pb_kits/playbook/pb_dropdown/index.js +171 -3
  73. data/app/pb_kits/playbook/pb_dropdown/quickpick_helper.rb +75 -0
  74. data/app/pb_kits/playbook/pb_filter/Filter/FilterBackground.tsx +3 -3
  75. data/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb +1 -1
  76. data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +2 -1
  77. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.html.erb +14 -0
  78. data/app/pb_kits/playbook/pb_form/docs/_form_with_required_indicator.md +3 -0
  79. data/app/pb_kits/playbook/pb_form/docs/example.yml +1 -0
  80. data/app/pb_kits/playbook/pb_gauge/_gauge.tsx +6 -0
  81. data/app/pb_kits/playbook/pb_line_graph/_line_graph.tsx +6 -0
  82. data/app/pb_kits/playbook/pb_radio/docs/_radio_error.md +1 -1
  83. data/app/pb_kits/playbook/pb_select/_select.tsx +8 -3
  84. data/app/pb_kits/playbook/pb_select/docs/_select_error.md +1 -1
  85. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.html.erb +16 -0
  86. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.jsx +30 -0
  87. data/app/pb_kits/playbook/pb_select/docs/_select_input_options.md +1 -0
  88. data/app/pb_kits/playbook/pb_select/docs/example.yml +2 -0
  89. data/app/pb_kits/playbook/pb_select/docs/index.js +1 -0
  90. data/app/pb_kits/playbook/pb_select/select.html.erb +2 -2
  91. data/app/pb_kits/playbook/pb_select/select.rb +3 -1
  92. data/app/pb_kits/playbook/pb_select/select.test.js +23 -0
  93. data/app/pb_kits/playbook/pb_table/_table.tsx +187 -33
  94. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.jsx +134 -0
  95. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant.md +34 -0
  96. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.html.erb +101 -0
  97. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +33 -0
  98. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.jsx +180 -0
  99. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination.md +3 -0
  100. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination_rails.html.erb +122 -0
  101. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_with_pagination_rails.md +3 -0
  102. data/app/pb_kits/playbook/pb_table/docs/example.yml +4 -0
  103. data/app/pb_kits/playbook/pb_table/docs/index.js +2 -0
  104. data/app/pb_kits/playbook/pb_table/table.html.erb +68 -12
  105. data/app/pb_kits/playbook/pb_table/table.rb +22 -3
  106. data/app/pb_kits/playbook/pb_table/table.test.js +143 -0
  107. data/app/pb_kits/playbook/pb_text_input/_text_input.tsx +15 -3
  108. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_error.md +1 -1
  109. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.html.erb +6 -0
  110. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.jsx +25 -0
  111. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_required_indicator.md +3 -0
  112. data/app/pb_kits/playbook/pb_text_input/docs/example.yml +3 -0
  113. data/app/pb_kits/playbook/pb_text_input/docs/index.js +1 -0
  114. data/app/pb_kits/playbook/pb_text_input/text_input.html.erb +6 -0
  115. data/app/pb_kits/playbook/pb_text_input/text_input.rb +2 -0
  116. data/app/pb_kits/playbook/pb_text_input/text_input.test.js +16 -0
  117. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_error.md +1 -1
  118. data/app/pb_kits/playbook/pb_time_picker/_time_picker.scss +296 -0
  119. data/app/pb_kits/playbook/pb_time_picker/_time_picker.tsx +822 -0
  120. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.html.erb +2 -0
  121. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.jsx +16 -0
  122. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_24_hour.md +1 -0
  123. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.html.erb +1 -0
  124. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.jsx +13 -0
  125. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default.md +1 -0
  126. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.html.erb +4 -0
  127. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.jsx +29 -0
  128. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_default_time.md +1 -0
  129. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.html.erb +13 -0
  130. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_disabled.jsx +23 -0
  131. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.html.erb +5 -0
  132. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_error.jsx +15 -0
  133. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_input_options.html.erb +14 -0
  134. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.html.erb +2 -0
  135. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_label.jsx +15 -0
  136. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.html.erb +42 -0
  137. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.jsx +52 -0
  138. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_min_max_time.md +1 -0
  139. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.jsx +45 -0
  140. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_on_handler.md +1 -0
  141. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.html.erb +3 -0
  142. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.jsx +21 -0
  143. data/app/pb_kits/playbook/pb_time_picker/docs/_time_picker_timezone.md +1 -0
  144. data/app/pb_kits/playbook/pb_time_picker/docs/example.yml +24 -0
  145. data/app/pb_kits/playbook/pb_time_picker/docs/index.js +9 -0
  146. data/app/pb_kits/playbook/pb_time_picker/index.ts +40 -0
  147. data/app/pb_kits/playbook/pb_time_picker/time_picker.html.erb +1 -0
  148. data/app/pb_kits/playbook/pb_time_picker/time_picker.rb +80 -0
  149. data/app/pb_kits/playbook/pb_time_picker/time_picker.test.jsx +114 -0
  150. data/app/pb_kits/playbook/pb_time_picker/time_picker_helper.ts +662 -0
  151. data/app/pb_kits/playbook/pb_timeline/_item.tsx +3 -0
  152. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.html.erb +60 -0
  153. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.jsx +118 -0
  154. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.md +1 -0
  155. data/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_date.md +1 -1
  156. data/app/pb_kits/playbook/pb_timeline/docs/example.yml +2 -0
  157. data/app/pb_kits/playbook/pb_timeline/docs/index.js +1 -0
  158. data/app/pb_kits/playbook/pb_timeline/item.html.erb +1 -1
  159. data/app/pb_kits/playbook/pb_timeline/item.rb +2 -0
  160. data/app/pb_kits/playbook/pb_timeline/label.html.erb +2 -1
  161. data/app/pb_kits/playbook/pb_timeline/label.rb +2 -0
  162. data/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx +3 -0
  163. data/app/pb_kits/playbook/pb_timeline/timeline.test.js +51 -0
  164. data/app/pb_kits/playbook/tokens/_colors.scss +2 -1
  165. data/app/pb_kits/playbook/utilities/deprecated.ts +73 -0
  166. data/app/pb_kits/playbook/utilities/globalProps.ts +1 -0
  167. data/dist/chunks/_typeahead-Ckz1ce-2.js +6 -0
  168. data/dist/chunks/lib-DxDBrGZX.js +29 -0
  169. data/dist/chunks/vendor.js +3 -3
  170. data/dist/menu.yml +16 -9
  171. data/dist/playbook-rails-react-bindings.js +1 -1
  172. data/dist/playbook-rails.js +1 -1
  173. data/dist/playbook.css +1 -1
  174. data/lib/playbook/forms/builder/collection_select_field.rb +9 -1
  175. data/lib/playbook/forms/builder/form_field_builder.rb +13 -2
  176. data/lib/playbook/forms/builder/select_field.rb +9 -1
  177. data/lib/playbook/forms/builder/time_picker_field.rb +24 -0
  178. data/lib/playbook/forms/builder/time_zone_select_field.rb +9 -1
  179. data/lib/playbook/forms/builder.rb +1 -0
  180. data/lib/playbook/pb_doc_helper.rb +3 -0
  181. data/lib/playbook/pb_kit_helper.rb +35 -0
  182. data/lib/playbook/version.rb +2 -2
  183. metadata +85 -4
  184. data/dist/chunks/_typeahead-BXM7QUuy.js +0 -6
  185. data/dist/chunks/lib-CgpqUb6l.js +0 -29
@@ -1,4 +1,4 @@
1
- import React, { createContext, useReducer, useContext, useEffect, useMemo } from "react";
1
+ import React, { createContext, useReducer, useContext, useEffect, useMemo, useRef } from "react";
2
2
  import { InitialStateType, ActionType, DraggableProviderType } from "./types";
3
3
 
4
4
  const initialState: InitialStateType = {
@@ -92,6 +92,35 @@ const reducer = (state: InitialStateType, action: ActionType) => {
92
92
  return { ...state, items: newItems };
93
93
  }
94
94
 
95
+ // Reset item back to its original container and position (e.g., when drag ends without valid drop)
96
+ case "RESET_DRAG_CONTAINER": {
97
+ const { itemId, originalContainer, originalIndex } = action.payload;
98
+ const newItems = [...state.items];
99
+ const draggedItem = newItems.find(item => item && item.id === itemId);
100
+
101
+ if (!draggedItem) return state;
102
+
103
+ const currentIndex = newItems.indexOf(draggedItem);
104
+
105
+ // Remove from current position
106
+ newItems.splice(currentIndex, 1);
107
+
108
+ // Restore container property and insert at original index
109
+ const restoredItem = { ...draggedItem, container: originalContainer };
110
+
111
+ // Insert at original index, or at end if index is invalid
112
+ if (originalIndex !== undefined && originalIndex >= 0) {
113
+ newItems.splice(originalIndex, 0, restoredItem);
114
+ } else {
115
+ newItems.push(restoredItem);
116
+ }
117
+
118
+ return {
119
+ ...state,
120
+ items: newItems
121
+ };
122
+ }
123
+
95
124
  default:
96
125
  return state;
97
126
  }
@@ -119,6 +148,29 @@ export const DraggableProvider = ({
119
148
  enableCrossContainerPreview = false,
120
149
  }: DraggableProviderType) => {
121
150
  const [state, dispatch] = useReducer(reducer, initialState);
151
+
152
+ // Track drag state for global listener
153
+ const dragStateRef = useRef<{
154
+ isDragging: boolean;
155
+ draggedItemId: string;
156
+ originalContainer: string;
157
+ originalIndex: number;
158
+ currentContainer: string;
159
+ dropOccurred: boolean;
160
+ }>({
161
+ isDragging: false,
162
+ draggedItemId: '',
163
+ originalContainer: '',
164
+ originalIndex: -1,
165
+ currentContainer: '',
166
+ dropOccurred: false,
167
+ });
168
+
169
+ // Track current state for use in gated event listeners (avoid stale closures)
170
+ const stateRef = useRef(state);
171
+ useEffect(() => {
172
+ stateRef.current = state;
173
+ }, [state]);
122
174
 
123
175
  // Parse dropZone prop - handle both string format (backward compatibility) and object format
124
176
  let dropZoneType = 'ghost';
@@ -148,7 +200,209 @@ export const DraggableProvider = ({
148
200
  onReorder(state.items);
149
201
  }, [state.items]);
150
202
 
203
+ // Monitor for failed drops by detecting mouse/pointer release during drag (this is needed for cross container preview)
204
+ useEffect(() => {
205
+ if (!enableCrossContainerPreview) return;
206
+
207
+ // Allow drops anywhere on the document by preventing default dragover
208
+ const handleGlobalDragOver = (e: DragEvent) => {
209
+ if (dragStateRef.current.isDragging) {
210
+ e.preventDefault();
211
+ }
212
+ };
213
+
214
+ // Handle drops anywhere on the document (including non-container areas)
215
+ const handleGlobalDrop = (e: DragEvent) => {
216
+ if (!dragStateRef.current.isDragging) return;
217
+
218
+ // If a container already handled the drop, don't process again
219
+ if (dragStateRef.current.dropOccurred) return;
220
+
221
+ e.preventDefault();
222
+
223
+ // If we reach here, it means the drop was NOT on a valid container
224
+ // (otherwise the container's handleDrop would have set dropOccurred = true)
225
+ // So we should ALWAYS reset to original container for invalid drops
226
+ const originalContainer = dragStateRef.current.originalContainer;
227
+
228
+ dispatch({
229
+ type: 'RESET_DRAG_CONTAINER',
230
+ payload: {
231
+ itemId: dragStateRef.current.draggedItemId,
232
+ originalContainer: originalContainer,
233
+ originalIndex: dragStateRef.current.originalIndex,
234
+ },
235
+ });
236
+
237
+ dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
238
+ dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
239
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
240
+
241
+ // Clear drag state
242
+ dragStateRef.current = {
243
+ isDragging: false,
244
+ draggedItemId: '',
245
+ originalContainer: '',
246
+ originalIndex: -1,
247
+ currentContainer: '',
248
+ dropOccurred: false,
249
+ };
250
+ };
251
+
252
+ const handleGlobalMouseUp = () => {
253
+ // If we're dragging and mouse is released, wait a bit to see if drop occurs
254
+ if (dragStateRef.current.isDragging) {
255
+ setTimeout(() => {
256
+ const currentContainer = dragStateRef.current.currentContainer;
257
+
258
+ // If drop still hasn't occurred, check if item is in a different container
259
+ if (dragStateRef.current.isDragging && !dragStateRef.current.dropOccurred) {
260
+ // If item is in a different container than original, treat it as a successful drop
261
+ if (currentContainer && currentContainer !== dragStateRef.current.originalContainer) {
262
+ // Trigger onDrop callback with the current container
263
+ if (onDrop) {
264
+ const draggedItem = stateRef.current.items.find(item => item && item.id === dragStateRef.current.draggedItemId);
265
+ const updatedItem = draggedItem ? { ...draggedItem, container: currentContainer } : null;
266
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === currentContainer);
267
+ const indexInContainer = itemsInContainer.findIndex(item => item && item.id === dragStateRef.current.draggedItemId);
268
+ const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
269
+ const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
270
+
271
+ onDrop(
272
+ dragStateRef.current.draggedItemId,
273
+ currentContainer,
274
+ dragStateRef.current.originalContainer,
275
+ updatedItem,
276
+ itemAbove,
277
+ itemBelow
278
+ );
279
+ }
280
+ } else {
281
+ dispatch({
282
+ type: 'RESET_DRAG_CONTAINER',
283
+ payload: {
284
+ itemId: dragStateRef.current.draggedItemId,
285
+ originalContainer: dragStateRef.current.originalContainer,
286
+ originalIndex: dragStateRef.current.originalIndex,
287
+ },
288
+ });
289
+ }
290
+ dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
291
+ dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
292
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
293
+
294
+ // Clear drag state
295
+ dragStateRef.current = {
296
+ isDragging: false,
297
+ draggedItemId: '',
298
+ originalContainer: '',
299
+ originalIndex: -1,
300
+ currentContainer: '',
301
+ dropOccurred: false,
302
+ };
303
+ }
304
+ }, 50); // Small delay to let drop event fire if it's going to
305
+ }
306
+ };
307
+
308
+ // Detect when drag leaves document boundaries
309
+ const handleDragLeave = (e: DragEvent) => {
310
+ // Check if we're leaving the document (relatedTarget will be null)
311
+ if (!e.relatedTarget && dragStateRef.current.isDragging && !dragStateRef.current.dropOccurred) {
312
+ // Drag left the document: reset to original container immediately
313
+ dispatch({
314
+ type: 'RESET_DRAG_CONTAINER',
315
+ payload: {
316
+ itemId: dragStateRef.current.draggedItemId,
317
+ originalContainer: dragStateRef.current.originalContainer,
318
+ originalIndex: dragStateRef.current.originalIndex,
319
+ },
320
+ });
321
+ dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
322
+ dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
323
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
324
+
325
+ // Clear drag state
326
+ dragStateRef.current = {
327
+ isDragging: false,
328
+ draggedItemId: '',
329
+ originalContainer: '',
330
+ originalIndex: -1,
331
+ currentContainer: '',
332
+ dropOccurred: false,
333
+ };
334
+ }
335
+ };
336
+
337
+ document.addEventListener('dragover', handleGlobalDragOver);
338
+ document.addEventListener('drop', handleGlobalDrop);
339
+ document.addEventListener('dragleave', handleDragLeave);
340
+ document.addEventListener('mouseup', handleGlobalMouseUp);
341
+ document.addEventListener('pointerup', handleGlobalMouseUp);
342
+
343
+ return () => {
344
+ document.removeEventListener('dragover', handleGlobalDragOver);
345
+ document.removeEventListener('drop', handleGlobalDrop);
346
+ document.removeEventListener('dragleave', handleDragLeave);
347
+ document.removeEventListener('mouseup', handleGlobalMouseUp);
348
+ document.removeEventListener('pointerup', handleGlobalMouseUp);
349
+ };
350
+ }, [enableCrossContainerPreview]);
351
+
352
+ // Detect when dragging stops (isDragging goes from truthy to empty)
353
+ const prevIsDraggingRef = useRef(state.isDragging);
354
+
355
+ useEffect(() => {
356
+ if (!enableCrossContainerPreview) return;
357
+
358
+ const wasDragging = prevIsDraggingRef.current;
359
+ const isNowDragging = state.isDragging;
360
+
361
+ // Drag just ended (was dragging, now not)
362
+ if (wasDragging && !isNowDragging) {
363
+
364
+ // If drop didn't occur, reset to original container
365
+ if (!dragStateRef.current.dropOccurred && dragStateRef.current.draggedItemId) {
366
+ dispatch({
367
+ type: 'RESET_DRAG_CONTAINER',
368
+ payload: {
369
+ itemId: dragStateRef.current.draggedItemId,
370
+ originalContainer: dragStateRef.current.originalContainer,
371
+ originalIndex: dragStateRef.current.originalIndex,
372
+ },
373
+ });
374
+ }
375
+
376
+ // Clear drag state
377
+ dragStateRef.current = {
378
+ isDragging: false,
379
+ draggedItemId: '',
380
+ originalContainer: '',
381
+ originalIndex: -1,
382
+ currentContainer: '',
383
+ dropOccurred: false,
384
+ };
385
+ }
386
+
387
+ prevIsDraggingRef.current = isNowDragging;
388
+ }, [state.isDragging, enableCrossContainerPreview]);
389
+
151
390
  const handleDragStart = (id: string, container: string) => {
391
+ // Track drag in ref for global listener
392
+ if (enableCrossContainerPreview) {
393
+ // Find the original index of the item
394
+ const originalIndex = state.items.findIndex(item => item && item.id === id);
395
+
396
+ dragStateRef.current = {
397
+ isDragging: true,
398
+ draggedItemId: id,
399
+ originalContainer: container,
400
+ originalIndex: originalIndex,
401
+ currentContainer: container,
402
+ dropOccurred: false,
403
+ };
404
+ }
405
+
152
406
  dispatch({ type: 'SET_DRAG_DATA', payload: { id: id, initialGroup: container, originId: providerId } });
153
407
  dispatch({ type: 'SET_IS_DRAGGING', payload: id });
154
408
  if (onDragStart) onDragStart(id, container);
@@ -181,6 +435,10 @@ export const DraggableProvider = ({
181
435
  newContainer: container,
182
436
  },
183
437
  });
438
+ // Update current container in ref
439
+ if (enableCrossContainerPreview) {
440
+ dragStateRef.current.currentContainer = container;
441
+ }
184
442
  } else {
185
443
  // Same container: keep original behavior
186
444
  dispatch({
@@ -193,7 +451,10 @@ export const DraggableProvider = ({
193
451
  dispatch({type: "REORDER_ITEMS", payload: { dragId: state.dragData.id, targetId: id }});
194
452
  }
195
453
 
196
- dispatch({type: "SET_DRAG_DATA",payload: {id: state.dragData.id, initialGroup: container, originId: providerId}});
454
+ // When enableCrossContainerPreview is true, preserve the original initialGroup
455
+ // Otherwise, update it to track the current container
456
+ const newInitialGroup = enableCrossContainerPreview ? state.dragData.initialGroup : container;
457
+ dispatch({type: "SET_DRAG_DATA",payload: {id: state.dragData.id, initialGroup: newInitialGroup, originId: providerId}});
197
458
  }
198
459
  if (onDragEnter) onDragEnter(id, container);
199
460
  };
@@ -202,17 +463,29 @@ export const DraggableProvider = ({
202
463
  const draggedItemId = state.dragData.id;
203
464
  const originalContainer = state.dragData.initialGroup;
204
465
 
466
+ // If enableCrossContainerPreview is true and no drop occurred, reset item to original container
467
+ if (enableCrossContainerPreview && !dragStateRef.current.dropOccurred && draggedItemId && originalContainer) {
468
+ dispatch({ type: 'RESET_DRAG_CONTAINER', payload: { itemId: draggedItemId, originalContainer, originalIndex: dragStateRef.current.originalIndex } });
469
+ }
470
+
205
471
  dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
206
472
  dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
207
473
  dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
474
+
475
+ // Only call onDragEnd if drop didn't already occur (for enableCrossContainerPreview)
476
+ // If drop occurred, handleDrop or global drop handler already called onDragEnd
477
+ if (enableCrossContainerPreview && dragStateRef.current.dropOccurred) {
478
+ return;
479
+ }
480
+
208
481
  if (onDragEnd) {
209
482
  if (!enableCrossContainerPreview) {
210
483
  onDragEnd();
211
484
  } else {
212
- const draggedItem = state.items.find(item => item && item.id === draggedItemId);
485
+ const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
213
486
  const finalContainer = draggedItem ? draggedItem.container : originalContainer;
214
487
 
215
- const itemsInContainer = state.items.filter(item => item && item.container === finalContainer);
488
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === finalContainer);
216
489
  const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
217
490
  const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
218
491
  const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
@@ -237,32 +510,58 @@ export const DraggableProvider = ({
237
510
 
238
511
  const draggedItemId = state.dragData.id;
239
512
  const originalContainer = state.dragData.initialGroup;
513
+
514
+ // Mark drop as successful in ref for global listener
515
+ if (enableCrossContainerPreview) {
516
+ dragStateRef.current.dropOccurred = true;
517
+ }
518
+
519
+ // Gather data for callbacks BEFORE clearing state
520
+ const isCrossContainer = container !== originalContainer;
521
+ let callbackData = null;
522
+
523
+ if (enableCrossContainerPreview) {
524
+ const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
525
+ const updatedItem = draggedItem ? { ...draggedItem, container } : null;
526
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === container);
527
+ const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
528
+ const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
529
+ const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
530
+
531
+ callbackData = { updatedItem, itemAbove, itemBelow };
532
+ }
240
533
 
241
534
  dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
242
535
  dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
536
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
243
537
  changeCategory(state.dragData.id, container);
538
+
244
539
  if (onDrop) {
245
540
  if (!enableCrossContainerPreview) {
246
541
  onDrop(container);
247
542
  } else {
248
- const draggedItem = state.items.find(item => item && item.id === draggedItemId);
249
- const updatedItem = draggedItem ? { ...draggedItem, container } : null;
250
-
251
- const itemsInContainer = state.items.filter(item => item && item.container === container);
252
- const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
253
- const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
254
- const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
255
-
256
543
  onDrop(
257
544
  draggedItemId,
258
545
  container,
259
546
  originalContainer,
260
- updatedItem,
261
- itemAbove,
262
- itemBelow
547
+ callbackData.updatedItem,
548
+ callbackData.itemAbove,
549
+ callbackData.itemBelow
263
550
  );
264
551
  }
265
552
  }
553
+
554
+ // Trigger onDragEnd ONLY for cross-container drops (dragend doesn't fire reliably in that case)
555
+ // For same-container drops, handleDragEnd will be called normally
556
+ if (enableCrossContainerPreview && isCrossContainer && onDragEnd && callbackData) {
557
+ onDragEnd(
558
+ draggedItemId,
559
+ container,
560
+ originalContainer,
561
+ callbackData.itemAbove,
562
+ callbackData.itemBelow
563
+ );
564
+ }
266
565
  };
267
566
 
268
567
  const handleDragOver = (e: Event, container: string) => {
@@ -281,6 +580,8 @@ export const DraggableProvider = ({
281
580
  type: "MOVE_TO_CONTAINER_END",
282
581
  payload: { dragId: state.dragData.id, newContainer: container },
283
582
  });
583
+ // Update current container in ref
584
+ dragStateRef.current.currentContainer = container;
284
585
  }
285
586
  }
286
587
 
@@ -23,7 +23,7 @@ export type ActionType =
23
23
  | { type: 'REORDER_ITEMS'; payload: { dragId: string; targetId: string } }
24
24
  | { type: 'REORDER_ITEMS_CROSS_CONTAINER'; payload: { dragId: string; targetId: string; newContainer: string } }
25
25
  | { type: 'MOVE_TO_CONTAINER_END'; payload: { dragId: string; newContainer: string } }
26
- | { type: 'RESET_DRAG_CONTAINER'; payload: { itemId: string; originalContainer: string } };
26
+ | { type: 'RESET_DRAG_CONTAINER'; payload: { itemId: string; originalContainer: string, originalIndex: number } };
27
27
 
28
28
  export interface DropZoneConfig {
29
29
  type?: 'ghost' | 'outline' | 'shadow' | 'line';
@@ -20,12 +20,14 @@
20
20
 
21
21
  %>
22
22
 
23
- <%= pb_rails("dropdown", props: {options: options}) %>
23
+ <%= pb_rails("dropdown", props: {id: "country-dropdown", options: options}) %>
24
24
 
25
25
  <script>
26
26
  document.addEventListener("pb:dropdown:selected", (e) => {
27
- const option = e.detail;
28
- const dropdown = e.target;
29
- console.log("Selected option:", option);
30
- })
27
+ if (e.target.id === "country-dropdown") {
28
+ const option = e.detail;
29
+ const dropdown = e.target;
30
+ console.log("Selected option:", option);
31
+ }
32
+ });
31
33
  </script>
@@ -0,0 +1,19 @@
1
+
2
+ <%= pb_rails("dropdown", props: {
3
+ id: "date-range-with-default",
4
+ label: "Date Range",
5
+ variant: "quickpick",
6
+ default_value: "This Year"
7
+ }) %>
8
+
9
+ <script>
10
+ document.addEventListener("DOMContentLoaded", () => {
11
+ const dropdown = document.getElementById("date-range-with-default");
12
+ if (dropdown) {
13
+ dropdown.addEventListener("pb:dropdown:selected", (e) => {
14
+ const option = e.detail;
15
+ console.log("Selected option:", option);
16
+ });
17
+ }
18
+ });
19
+ </script>
@@ -0,0 +1,12 @@
1
+ <%= pb_rails("dropdown", props: {id: "date-range-quickpick-1", label: "Date Range", variant: "quickpick"}) %>
2
+
3
+
4
+ <script>
5
+ const dropdown = document.getElementById("date-range-quickpick-1");
6
+ if (dropdown) {
7
+ dropdown.addEventListener("pb:dropdown:selected", (e) => {
8
+ const option = e.detail;
9
+ console.log("Selected option:", option);
10
+ });
11
+ }
12
+ </script>
@@ -0,0 +1,26 @@
1
+ The `quickpick` variant provides predefined date based options when `variant:"quickpick"` is used.
2
+
3
+ Open the Dropdown above to see the default options.
4
+
5
+ The quickpick variant automatically generates hidden inputs for `start_date` and `end_date` which are populated when a date range is selected. These inputs are ready for form submission.
6
+
7
+ You can customize the input names and IDs using the following props:
8
+ - `start_date_name` - The name attribute for the start date input (default: `"start_date_name"`)
9
+ - `start_date_id` - The ID attribute for the start date input (default: `"start_date_id"`)
10
+ - `end_date_name` - The name attribute for the end date input (default: `"end_date_name"`)
11
+ - `end_date_id` - The ID attribute for the end date input (default: `"end_date_id"`)
12
+
13
+ Example with custom names:
14
+ ```ruby
15
+ pb_rails("dropdown", props: {
16
+ variant: "quickpick",
17
+ start_date_name: "filter[start_date]",
18
+ start_date_id: "filter_start_date",
19
+ end_date_name: "filter[end_date]",
20
+ end_date_id: "filter_end_date"
21
+ })
22
+ ```
23
+
24
+ The Dropdown kit also comes with a custom event called "pb:dropdown:selected" which updates dynamically with the selection as it changes. See code snippet to see this in action.
25
+
26
+ In addition, a data attribute called data-option-selected with the selection is also rendered on the parent dropdown div.
@@ -0,0 +1,19 @@
1
+
2
+ <%= pb_rails("dropdown", props: {
3
+ id: "date-range-quickpick-end-today",
4
+ label: "Date Range",
5
+ variant: "quickpick",
6
+ range_ends_today: true
7
+ }) %>
8
+
9
+ <script>
10
+ document.addEventListener("DOMContentLoaded", () => {
11
+ const dropdown = document.getElementById("date-range-quickpick-end-today");
12
+ if (dropdown) {
13
+ dropdown.addEventListener("pb:dropdown:selected", (e) => {
14
+ const option = e.detail;
15
+ console.log("Selected option:", option);
16
+ });
17
+ }
18
+ });
19
+ </script>
@@ -0,0 +1 @@
1
+ The optional `range_ends_today` prop can be used with the `quickpick` variant to set end date on all ranges that start with 'this' to today's date. For instance, by default 'This Year' will set end day to 12/31/(current year), but if `range_ends_today` prop is used, end date on that range will be today's date.
@@ -0,0 +1,30 @@
1
+ <%= pb_rails("dropdown", props: {
2
+ id: "dropdown-quickpick-with-date-pickers-default",
3
+ label: "Date Range",
4
+ name: "date_range",
5
+ margin_bottom: "sm",
6
+ variant: "quickpick",
7
+ default_value: "This Month",
8
+ controls_start_id: "start-date-picker-default",
9
+ controls_end_id: "end-date-picker-default",
10
+ start_date_id: "quickpick_start_date_default",
11
+ start_date_name: "start_date",
12
+ end_date_id: "quickpick_end_date_default",
13
+ end_date_name: "end_date"
14
+ }) %>
15
+
16
+ <%= pb_rails("date_picker", props: {
17
+ picker_id: "start-date-picker-default",
18
+ label: "Start Date",
19
+ name: "start_date_picker",
20
+ placeholder: "Select Start Date",
21
+ sync_start_with: "dropdown-quickpick-with-date-pickers-default"
22
+ }) %>
23
+
24
+ <%= pb_rails("date_picker", props: {
25
+ picker_id: "end-date-picker-default",
26
+ label: "End Date",
27
+ name: "end_date_picker",
28
+ placeholder: "Select End Date",
29
+ sync_end_with: "dropdown-quickpick-with-date-pickers-default"
30
+ }) %>
@@ -0,0 +1,3 @@
1
+ This example demonstrates the 3-input pattern with a default value. The dropdown is initialized with "This Month" selected, and both DatePickers are automatically populated with the corresponding start and end dates.
2
+
3
+ The default value can be set using the `default_value` prop with any of the quickpick option labels.
@@ -0,0 +1,29 @@
1
+ <%= pb_rails("dropdown", props: {
2
+ id: "dropdown-quickpick-with-date-pickers",
3
+ label: "Date Range",
4
+ name: "date_range",
5
+ margin_bottom: "sm",
6
+ variant: "quickpick",
7
+ controls_start_id: "start-date-picker",
8
+ controls_end_id: "end-date-picker",
9
+ start_date_id: "quickpick_start_date",
10
+ start_date_name: "start_date",
11
+ end_date_id: "quickpick_end_date",
12
+ end_date_name: "end_date"
13
+ }) %>
14
+
15
+ <%= pb_rails("date_picker", props: {
16
+ picker_id: "start-date-picker",
17
+ label: "Start Date",
18
+ name: "start_date_picker",
19
+ placeholder: "Select Start Date",
20
+ sync_start_with: "dropdown-quickpick-with-date-pickers"
21
+ }) %>
22
+
23
+ <%= pb_rails("date_picker", props: {
24
+ picker_id: "end-date-picker",
25
+ label: "End Date",
26
+ name: "end_date_picker",
27
+ placeholder: "Select End Date",
28
+ sync_end_with: "dropdown-quickpick-with-date-pickers"
29
+ }) %>
@@ -0,0 +1,13 @@
1
+ The quickpick variant can be synced with two DatePickers for a 3-input pattern. When a quickpick option is selected from the dropdown, both DatePickers are automatically populated. When either DatePicker is manually changed, the dropdown is cleared.
2
+
3
+ #### Props for 3-Input Pattern:
4
+
5
+ - `controls_start_id` - ID of the start DatePicker to sync with
6
+ - `controls_end_id` - ID of the end DatePicker to sync with
7
+
8
+ #### DatePicker Props:
9
+
10
+ - `sync_start_with` - ID of the dropdown to clear when start date changes
11
+ - `sync_end_with` - ID of the dropdown to clear when end date changes
12
+
13
+ This pattern allows users to quickly select common date ranges or manually pick specific dates.
@@ -47,7 +47,7 @@
47
47
  %>
48
48
 
49
49
 
50
- <%= pb_rails("dropdown", props: {options: options}) do %>
50
+ <%= pb_rails("dropdown", props: {id: "user-dropdown", options: options}) do %>
51
51
  <%= pb_rails("dropdown/dropdown_trigger", props: {placeholder: "Select a User", custom_display: custom_display}) %>
52
52
  <%= pb_rails("dropdown/dropdown_container") do %>
53
53
  <% options.each do |option| %>
@@ -71,6 +71,8 @@
71
71
 
72
72
  <script>
73
73
  document.addEventListener("pb:dropdown:selected", (e) => {
74
+ if (e.target.id !== "user-dropdown") return;
75
+
74
76
  const option = e.detail;
75
77
  const dropdown = e.target;
76
78
 
@@ -22,6 +22,11 @@ examples:
22
22
  - dropdown_multi_select_with_default: Multi Select Default Value
23
23
  - dropdown_blank_selection: Blank Selection
24
24
  - dropdown_separators_hidden: Separators Hidden
25
+ - dropdown_quickpick_rails: Quick Pick Variant
26
+ - dropdown_quickpick_range_end_rails: Quick Pick Variant (Range Ends Today)
27
+ - dropdown_quickpick_default_dates: Quick Pick Variant (Default Dates)
28
+ - dropdown_quickpick_with_date_pickers_rails: Quick Pick with Date Pickers
29
+ - dropdown_quickpick_with_date_pickers_default_rails: Quick Pick with Date Pickers (Default Value)
25
30
 
26
31
  react:
27
32
  - dropdown_default: Default
@@ -10,6 +10,10 @@
10
10
  style="display: none"
11
11
  <%= object.required ? "required" : ""%>
12
12
  />
13
+ <% if object.variant == "quickpick" %>
14
+ <input id="<%= object.start_date_id %>" name="<%= object.start_date_name %>" style="display: none">
15
+ <input id="<%= object.end_date_id %>" name="<%= object.end_date_name %>" style="display: none">
16
+ <% end %>
13
17
  <% if content.present? %>
14
18
  <%= content.presence %>
15
19
  <%= pb_rails("body", props: { status: "negative", text: object.error }) %>