playbook_ui 15.5.0 → 15.6.0.pre.alpha.draggableask12906

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +96 -6
  3. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_table_props.html.erb +1 -1
  4. data/app/pb_kits/playbook/pb_background/_background.tsx +6 -6
  5. data/app/pb_kits/playbook/pb_background/background.test.js +5 -1
  6. data/app/pb_kits/playbook/pb_background/docs/_background_light.html.erb +1 -1
  7. data/app/pb_kits/playbook/pb_background/docs/_background_light.jsx +0 -1
  8. data/app/pb_kits/playbook/pb_background/docs/_background_light.md +1 -0
  9. data/app/pb_kits/playbook/pb_background/docs/example.yml +2 -2
  10. data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +31 -0
  11. data/app/pb_kits/playbook/pb_draggable/context/index.tsx +399 -7
  12. data/app/pb_kits/playbook/pb_draggable/context/types.ts +8 -3
  13. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.jsx +180 -0
  14. data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.md +22 -0
  15. data/app/pb_kits/playbook/pb_draggable/docs/example.yml +3 -2
  16. data/app/pb_kits/playbook/pb_draggable/docs/index.js +2 -1
  17. data/app/pb_kits/playbook/pb_draggable/draggable.test.jsx +77 -1
  18. data/app/pb_kits/playbook/pb_file_upload/_file_upload.scss +4 -4
  19. data/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx +34 -22
  20. data/app/pb_kits/playbook/pb_home_address_street/city_emphasis.html.erb +16 -12
  21. data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_default.html.erb +1 -1
  22. data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.html.erb +16 -12
  23. data/app/pb_kits/playbook/pb_home_address_street/street_emphasis.html.erb +16 -12
  24. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +10 -0
  25. data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +66 -15
  26. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.jsx +42 -0
  27. data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.md +1 -0
  28. data/app/pb_kits/playbook/pb_multiple_users/docs/example.yml +1 -0
  29. data/app/pb_kits/playbook/pb_multiple_users/docs/index.js +1 -0
  30. data/app/pb_kits/playbook/pb_multiple_users/multiple_users.test.js +25 -0
  31. data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +44 -10
  32. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb +34 -4
  33. data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.jsx +16 -7
  34. data/app/pb_kits/playbook/pb_radio/docs/_radio_error.md +1 -1
  35. data/app/pb_kits/playbook/pb_select/docs/_select_error.md +1 -1
  36. data/app/pb_kits/playbook/pb_table/styles/_vertical_border.scss +49 -0
  37. data/app/pb_kits/playbook/pb_text_input/docs/_text_input_error.md +1 -1
  38. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_error.md +1 -1
  39. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +15 -0
  40. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +3 -0
  41. data/app/pb_kits/playbook/pb_typeahead/components/ClearIndicator.tsx +13 -2
  42. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +6 -1
  43. data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +34 -7
  44. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.html.erb +30 -0
  45. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.jsx +37 -0
  46. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.md +3 -0
  47. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  48. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +2 -1
  49. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +6 -1
  50. data/dist/chunks/_typeahead-wpGumTwA.js +6 -0
  51. data/dist/chunks/{lib-Dk4GKPut.js → lib-CgpqUb6l.js} +2 -2
  52. data/dist/chunks/vendor.js +2 -2
  53. data/dist/playbook-rails-react-bindings.js +1 -1
  54. data/dist/playbook-rails.js +1 -1
  55. data/dist/playbook.css +1 -1
  56. data/lib/playbook/version.rb +2 -2
  57. metadata +12 -5
  58. data/app/pb_kits/playbook/pb_bar_graph/BarGraphStyles.scss +0 -58
  59. data/dist/chunks/_typeahead-Bx4QsIEU.js +0 -6
@@ -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 = {
@@ -39,6 +39,72 @@ const reducer = (state: InitialStateType, action: ActionType) => {
39
39
 
40
40
  return { ...state, items: newItems };
41
41
  }
42
+
43
+ // Used only when enableCrossContainerPreview is true
44
+ case "REORDER_ITEMS_CROSS_CONTAINER": {
45
+ const { dragId, targetId, newContainer } = action.payload;
46
+ const newItems = [...state.items];
47
+ const draggedItem = newItems.find((item) => item && item.id === dragId);
48
+
49
+ if (!draggedItem) return state;
50
+
51
+ const draggedIndex = newItems.indexOf(draggedItem);
52
+ const targetIndex = newItems.findIndex(
53
+ (item) => item && item.id === targetId
54
+ );
55
+
56
+ if (draggedIndex === -1 || targetIndex === -1) return state;
57
+
58
+ const updatedItem = { ...draggedItem, container: newContainer };
59
+ newItems.splice(draggedIndex, 1);
60
+ newItems.splice(targetIndex, 0, updatedItem);
61
+
62
+ return { ...state, items: newItems };
63
+ }
64
+
65
+ // Used only when enableCrossContainerPreview is true
66
+ case "MOVE_TO_CONTAINER_END": {
67
+ const { dragId, newContainer } = action.payload;
68
+ const newItems = [...state.items];
69
+ const draggedItem = newItems.find((item) => item && item.id === dragId);
70
+
71
+ if (!draggedItem) return state;
72
+
73
+ const draggedIndex = newItems.indexOf(draggedItem);
74
+ if (draggedIndex === -1) return state;
75
+
76
+ const updatedItem = { ...draggedItem, container: newContainer };
77
+
78
+ // Remove from current position
79
+ newItems.splice(draggedIndex, 1);
80
+
81
+ // Insert at end of target container
82
+ const lastIndexInContainer = newItems
83
+ .map((item) => item && item.container)
84
+ .lastIndexOf(newContainer);
85
+
86
+ if (lastIndexInContainer === -1) {
87
+ newItems.push(updatedItem);
88
+ } else {
89
+ newItems.splice(lastIndexInContainer + 1, 0, updatedItem);
90
+ }
91
+
92
+ return { ...state, items: newItems };
93
+ }
94
+
95
+ // Reset item back to its original container (e.g., when drag ends without valid drop)
96
+ case "RESET_DRAG_CONTAINER": {
97
+ const { itemId, originalContainer } = action.payload;
98
+ return {
99
+ ...state,
100
+ items: state.items.map(item =>
101
+ item.id === itemId
102
+ ? { ...item, container: originalContainer }
103
+ : item
104
+ )
105
+ };
106
+ }
107
+
42
108
  default:
43
109
  return state;
44
110
  }
@@ -61,9 +127,32 @@ export const DraggableProvider = ({
61
127
  onDrop,
62
128
  onDragOver,
63
129
  dropZone = { type: 'ghost', color: 'neutral', direction: 'vertical' },
64
- providerId = 'default', // fallback provided for backward compatibility, so this does not become a required prop
130
+ providerId = 'default', // fallback provided for backward compatibility
131
+ // Opt-in flag for cross-container preview
132
+ enableCrossContainerPreview = false,
65
133
  }: DraggableProviderType) => {
66
134
  const [state, dispatch] = useReducer(reducer, initialState);
135
+
136
+ // Track drag state for global listener
137
+ const dragStateRef = useRef<{
138
+ isDragging: boolean;
139
+ draggedItemId: string;
140
+ originalContainer: string;
141
+ currentContainer: string;
142
+ dropOccurred: boolean;
143
+ }>({
144
+ isDragging: false,
145
+ draggedItemId: '',
146
+ originalContainer: '',
147
+ currentContainer: '',
148
+ dropOccurred: false,
149
+ });
150
+
151
+ // Track current state for use in gated event listeners (avoid stale closures)
152
+ const stateRef = useRef(state);
153
+ useEffect(() => {
154
+ stateRef.current = state;
155
+ }, [state]);
67
156
 
68
157
  // Parse dropZone prop - handle both string format (backward compatibility) and object format
69
158
  let dropZoneType = 'ghost';
@@ -93,7 +182,168 @@ export const DraggableProvider = ({
93
182
  onReorder(state.items);
94
183
  }, [state.items]);
95
184
 
185
+ // Monitor for failed drops by detecting mouse/pointer release during drag (this is needed for cross container preview)
186
+ useEffect(() => {
187
+ if (!enableCrossContainerPreview) return;
188
+
189
+ // Allow drops anywhere on the document by preventing default dragover
190
+ const handleGlobalDragOver = (e: DragEvent) => {
191
+ if (dragStateRef.current.isDragging) {
192
+ e.preventDefault();
193
+ }
194
+ };
195
+
196
+ // Handle drops anywhere on the document (including non-container areas)
197
+ const handleGlobalDrop = (e: DragEvent) => {
198
+ if (!dragStateRef.current.isDragging) return;
199
+
200
+ // If a container already handled the drop, don't process again
201
+ if (dragStateRef.current.dropOccurred) return;
202
+
203
+ e.preventDefault();
204
+
205
+ // If we reach here, it means the drop was NOT on a valid container
206
+ // (otherwise the container's handleDrop would have set dropOccurred = true)
207
+ // So we should ALWAYS reset to original container for invalid drops
208
+ const originalContainer = dragStateRef.current.originalContainer;
209
+
210
+ dispatch({
211
+ type: 'RESET_DRAG_CONTAINER',
212
+ payload: {
213
+ itemId: dragStateRef.current.draggedItemId,
214
+ originalContainer: originalContainer,
215
+ },
216
+ });
217
+
218
+ dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
219
+ dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
220
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
221
+
222
+ // Clear drag state
223
+ dragStateRef.current = {
224
+ isDragging: false,
225
+ draggedItemId: '',
226
+ originalContainer: '',
227
+ currentContainer: '',
228
+ dropOccurred: false,
229
+ };
230
+ };
231
+
232
+ const handleGlobalMouseUp = () => {
233
+ // If we're dragging and mouse is released, wait a bit to see if drop occurs
234
+ if (dragStateRef.current.isDragging) {
235
+ setTimeout(() => {
236
+ const currentContainer = dragStateRef.current.currentContainer;
237
+
238
+ // If drop still hasn't occurred, check if item is in a different container
239
+ if (dragStateRef.current.isDragging && !dragStateRef.current.dropOccurred) {
240
+ // If item is in a different container than original, treat it as a successful drop
241
+ if (currentContainer && currentContainer !== dragStateRef.current.originalContainer) {
242
+ // Trigger onDrop callback with the current container
243
+ if (onDrop) {
244
+ const draggedItem = stateRef.current.items.find(item => item && item.id === dragStateRef.current.draggedItemId);
245
+ const updatedItem = draggedItem ? { ...draggedItem, container: currentContainer } : null;
246
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === currentContainer);
247
+ const indexInContainer = itemsInContainer.findIndex(item => item && item.id === dragStateRef.current.draggedItemId);
248
+ const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
249
+ const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
250
+
251
+ onDrop(
252
+ dragStateRef.current.draggedItemId,
253
+ currentContainer,
254
+ dragStateRef.current.originalContainer,
255
+ updatedItem,
256
+ itemAbove,
257
+ itemBelow
258
+ );
259
+ }
260
+ } else {
261
+ dispatch({
262
+ type: 'RESET_DRAG_CONTAINER',
263
+ payload: {
264
+ itemId: dragStateRef.current.draggedItemId,
265
+ originalContainer: dragStateRef.current.originalContainer,
266
+ },
267
+ });
268
+ }
269
+ dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
270
+ dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
271
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
272
+
273
+ // Clear drag state
274
+ dragStateRef.current = {
275
+ isDragging: false,
276
+ draggedItemId: '',
277
+ originalContainer: '',
278
+ currentContainer: '',
279
+ dropOccurred: false,
280
+ };
281
+ }
282
+ }, 50); // Small delay to let drop event fire if it's going to
283
+ }
284
+ };
285
+
286
+ document.addEventListener('dragover', handleGlobalDragOver);
287
+ document.addEventListener('drop', handleGlobalDrop);
288
+ document.addEventListener('mouseup', handleGlobalMouseUp);
289
+ document.addEventListener('pointerup', handleGlobalMouseUp);
290
+
291
+ return () => {
292
+ document.removeEventListener('dragover', handleGlobalDragOver);
293
+ document.removeEventListener('drop', handleGlobalDrop);
294
+ document.removeEventListener('mouseup', handleGlobalMouseUp);
295
+ document.removeEventListener('pointerup', handleGlobalMouseUp);
296
+ };
297
+ }, [enableCrossContainerPreview]);
298
+
299
+ // Detect when dragging stops (isDragging goes from truthy to empty)
300
+ const prevIsDraggingRef = useRef(state.isDragging);
301
+
302
+ useEffect(() => {
303
+ if (!enableCrossContainerPreview) return;
304
+
305
+ const wasDragging = prevIsDraggingRef.current;
306
+ const isNowDragging = state.isDragging;
307
+
308
+ // Drag just ended (was dragging, now not)
309
+ if (wasDragging && !isNowDragging) {
310
+
311
+ // If drop didn't occur, reset to original container
312
+ if (!dragStateRef.current.dropOccurred && dragStateRef.current.draggedItemId) {
313
+ dispatch({
314
+ type: 'RESET_DRAG_CONTAINER',
315
+ payload: {
316
+ itemId: dragStateRef.current.draggedItemId,
317
+ originalContainer: dragStateRef.current.originalContainer,
318
+ },
319
+ });
320
+ }
321
+
322
+ // Clear drag state
323
+ dragStateRef.current = {
324
+ isDragging: false,
325
+ draggedItemId: '',
326
+ originalContainer: '',
327
+ currentContainer: '',
328
+ dropOccurred: false,
329
+ };
330
+ }
331
+
332
+ prevIsDraggingRef.current = isNowDragging;
333
+ }, [state.isDragging, enableCrossContainerPreview]);
334
+
96
335
  const handleDragStart = (id: string, container: string) => {
336
+ // Track drag in ref for global listener
337
+ if (enableCrossContainerPreview) {
338
+ dragStateRef.current = {
339
+ isDragging: true,
340
+ draggedItemId: id,
341
+ originalContainer: container,
342
+ currentContainer: container,
343
+ dropOccurred: false,
344
+ };
345
+ }
346
+
97
347
  dispatch({ type: 'SET_DRAG_DATA', payload: { id: id, initialGroup: container, originId: providerId } });
98
348
  dispatch({ type: 'SET_IS_DRAGGING', payload: id });
99
349
  if (onDragStart) onDragStart(id, container);
@@ -103,17 +353,93 @@ export const DraggableProvider = ({
103
353
  if (state.dragData.originId !== providerId) return; // Ignore drag events from other providers
104
354
 
105
355
  if (state.dragData.id !== id) {
106
- dispatch({ type: 'REORDER_ITEMS', payload: { dragId: state.dragData.id, targetId: id } });
107
- dispatch({ type: 'SET_DRAG_DATA', payload: { id: state.dragData.id, initialGroup: container, originId: providerId } });
356
+ if (enableCrossContainerPreview) {
357
+ // Used only when enableCrossContainerPreview is true
358
+ const draggedItem = state.items.find(
359
+ (item) => item && item.id === state.dragData.id
360
+ );
361
+ const currentContainer =
362
+ draggedItem && draggedItem.container
363
+ ? draggedItem.container
364
+ : state.dragData.initialGroup;
365
+
366
+ const isCrossContainer =
367
+ currentContainer !== container &&
368
+ (currentContainer !== undefined || container !== undefined);
369
+
370
+ if (isCrossContainer) {
371
+ dispatch({
372
+ type: "REORDER_ITEMS_CROSS_CONTAINER",
373
+ payload: {
374
+ dragId: state.dragData.id,
375
+ targetId: id,
376
+ newContainer: container,
377
+ },
378
+ });
379
+ // Update current container in ref
380
+ if (enableCrossContainerPreview) {
381
+ dragStateRef.current.currentContainer = container;
382
+ }
383
+ } else {
384
+ // Same container: keep original behavior
385
+ dispatch({
386
+ type: "REORDER_ITEMS",
387
+ payload: { dragId: state.dragData.id, targetId: id },
388
+ });
389
+ }
390
+ } else {
391
+ // Original behavior (no preview across containers)
392
+ dispatch({type: "REORDER_ITEMS", payload: { dragId: state.dragData.id, targetId: id }});
393
+ }
394
+
395
+ // When enableCrossContainerPreview is true, preserve the original initialGroup
396
+ // Otherwise, update it to track the current container
397
+ const newInitialGroup = enableCrossContainerPreview ? state.dragData.initialGroup : container;
398
+ dispatch({type: "SET_DRAG_DATA",payload: {id: state.dragData.id, initialGroup: newInitialGroup, originId: providerId}});
108
399
  }
109
400
  if (onDragEnter) onDragEnter(id, container);
110
401
  };
111
402
 
112
403
  const handleDragEnd = () => {
404
+ const draggedItemId = state.dragData.id;
405
+ const originalContainer = state.dragData.initialGroup;
406
+
407
+ // If enableCrossContainerPreview is true and no drop occurred, reset item to original container
408
+ if (enableCrossContainerPreview && !dragStateRef.current.dropOccurred && draggedItemId && originalContainer) {
409
+ dispatch({ type: 'RESET_DRAG_CONTAINER', payload: { itemId: draggedItemId, originalContainer } });
410
+ }
411
+
113
412
  dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
114
413
  dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
115
414
  dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
116
- if (onDragEnd) onDragEnd();
415
+
416
+ // Only call onDragEnd if drop didn't already occur (for enableCrossContainerPreview)
417
+ // If drop occurred, handleDrop or global drop handler already called onDragEnd
418
+ if (enableCrossContainerPreview && dragStateRef.current.dropOccurred) {
419
+ return;
420
+ }
421
+
422
+ if (onDragEnd) {
423
+ if (!enableCrossContainerPreview) {
424
+ onDragEnd();
425
+ } else {
426
+ const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
427
+ const finalContainer = draggedItem ? draggedItem.container : originalContainer;
428
+
429
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === finalContainer);
430
+ const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
431
+ const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
432
+ const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
433
+
434
+ onDragEnd(
435
+ draggedItemId,
436
+ finalContainer,
437
+ originalContainer,
438
+ itemAbove,
439
+ itemBelow
440
+ );
441
+ }
442
+ }
117
443
  };
118
444
 
119
445
  const changeCategory = (itemId: string, container: string) => {
@@ -123,10 +449,60 @@ export const DraggableProvider = ({
123
449
  const handleDrop = (container: string) => {
124
450
  if (state.dragData.originId !== providerId) return; // Ignore drop events from other providers
125
451
 
452
+ const draggedItemId = state.dragData.id;
453
+ const originalContainer = state.dragData.initialGroup;
454
+
455
+ // Mark drop as successful in ref for global listener
456
+ if (enableCrossContainerPreview) {
457
+ dragStateRef.current.dropOccurred = true;
458
+ }
459
+
460
+ // Gather data for callbacks BEFORE clearing state
461
+ const isCrossContainer = container !== originalContainer;
462
+ let callbackData = null;
463
+
464
+ if (enableCrossContainerPreview) {
465
+ const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
466
+ const updatedItem = draggedItem ? { ...draggedItem, container } : null;
467
+ const itemsInContainer = stateRef.current.items.filter(item => item && item.container === container);
468
+ const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
469
+ const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
470
+ const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
471
+
472
+ callbackData = { updatedItem, itemAbove, itemBelow };
473
+ }
474
+
126
475
  dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
127
476
  dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
477
+ dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
128
478
  changeCategory(state.dragData.id, container);
129
- if (onDrop) onDrop(container);
479
+
480
+ if (onDrop) {
481
+ if (!enableCrossContainerPreview) {
482
+ onDrop(container);
483
+ } else {
484
+ onDrop(
485
+ draggedItemId,
486
+ container,
487
+ originalContainer,
488
+ callbackData.updatedItem,
489
+ callbackData.itemAbove,
490
+ callbackData.itemBelow
491
+ );
492
+ }
493
+ }
494
+
495
+ // Trigger onDragEnd ONLY for cross-container drops (dragend doesn't fire reliably in that case)
496
+ // For same-container drops, handleDragEnd will be called normally
497
+ if (enableCrossContainerPreview && isCrossContainer && onDragEnd && callbackData) {
498
+ onDragEnd(
499
+ draggedItemId,
500
+ container,
501
+ originalContainer,
502
+ callbackData.itemAbove,
503
+ callbackData.itemBelow
504
+ );
505
+ }
130
506
  };
131
507
 
132
508
  const handleDragOver = (e: Event, container: string) => {
@@ -134,6 +510,22 @@ export const DraggableProvider = ({
134
510
 
135
511
  e.preventDefault();
136
512
  dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: container });
513
+
514
+ if (enableCrossContainerPreview && state.dragData.id) {
515
+ // Only when enableCrossContainerPreview is true: when hovering over a different container, move item to end
516
+ const draggedItem = state.items.find(
517
+ (item) => item && item.id === state.dragData.id
518
+ );
519
+ if (draggedItem && draggedItem.container !== container) {
520
+ dispatch({
521
+ type: "MOVE_TO_CONTAINER_END",
522
+ payload: { dragId: state.dragData.id, newContainer: container },
523
+ });
524
+ // Update current container in ref
525
+ dragStateRef.current.currentContainer = container;
526
+ }
527
+ }
528
+
137
529
  if (onDragOver) onDragOver(e, container);
138
530
  };
139
531
 
@@ -157,4 +549,4 @@ export const DraggableProvider = ({
157
549
  return (
158
550
  <DragContext.Provider value={contextValue}>{children}</DragContext.Provider>
159
551
  );
160
- };
552
+ };
@@ -18,8 +18,12 @@ export type ActionType =
18
18
  } }
19
19
  | { type: 'SET_IS_DRAGGING'; payload: string }
20
20
  | { type: 'SET_ACTIVE_CONTAINER'; payload: string }
21
+ | { type: 'SET_CROSS_CONTAINER_PREVIEW'; payload: boolean }
21
22
  | { type: 'CHANGE_CATEGORY'; payload: { itemId: string; container: string } }
22
- | { type: 'REORDER_ITEMS'; payload: { dragId: string; targetId: string } };
23
+ | { type: 'REORDER_ITEMS'; payload: { dragId: string; targetId: string } }
24
+ | { type: 'REORDER_ITEMS_CROSS_CONTAINER'; payload: { dragId: string; targetId: string; newContainer: string } }
25
+ | { type: 'MOVE_TO_CONTAINER_END'; payload: { dragId: string; newContainer: string } }
26
+ | { type: 'RESET_DRAG_CONTAINER'; payload: { itemId: string; originalContainer: string } };
23
27
 
24
28
  export interface DropZoneConfig {
25
29
  type?: 'ghost' | 'outline' | 'shadow' | 'line';
@@ -33,9 +37,10 @@ export type ActionType =
33
37
  onReorder: (items: ItemType[]) => void;
34
38
  onDragStart?: (id: string, container: string) => void;
35
39
  onDragEnter?: (id: string, container: string) => void;
36
- onDragEnd?: () => void;
37
- onDrop?: (container: string) => void;
40
+ onDragEnd?: (...args: any[]) => void;
41
+ onDrop?: (...args: any[]) => void;
38
42
  onDragOver?: (e: Event, container: string) => void;
39
43
  dropZone?: DropZoneConfig | string; // Can accept string for backward compatibility
40
44
  providerId?: string;
45
+ enableCrossContainerPreview?: boolean;
41
46
  }
@@ -0,0 +1,180 @@
1
+ import React, { useState } from "react";
2
+
3
+ import Flex from '../../pb_flex/_flex'
4
+ import Draggable from '../../pb_draggable/_draggable'
5
+ import { DraggableProvider } from '../../pb_draggable/context'
6
+ import Badge from '../../pb_badge/_badge'
7
+ import Title from '../../pb_title/_title'
8
+ import Caption from '../../pb_caption/_caption'
9
+ import Card from '../../pb_card/_card'
10
+ import FlexItem from '../../pb_flex/_flex_item'
11
+ import Avatar from '../../pb_avatar/_avatar'
12
+ import Body from '../../pb_body/_body'
13
+
14
+ // Initial groups to drag between
15
+ const containers = ["To Do", "In Progress", "Done"];
16
+
17
+ // Initial items to be dragged
18
+ const data = [
19
+ {
20
+ id: "11",
21
+ container: "To Do",
22
+ title: "Task 1",
23
+ description: "Bug fixes",
24
+ assignee_name: "Terry Miles",
25
+ assignee_img: "https://randomuser.me/api/portraits/men/44.jpg",
26
+ },
27
+ {
28
+ id: "12",
29
+ container: "To Do",
30
+ title: "Task 2",
31
+ description: "Documentation",
32
+ assignee_name: "Sophia Miles",
33
+ assignee_img: "https://randomuser.me/api/portraits/women/8.jpg",
34
+ },
35
+ {
36
+ id: "13",
37
+ container: "In Progress",
38
+ title: "Task 3",
39
+ description: "Add a variant",
40
+ assignee_name: "Alice Jones",
41
+ assignee_img: "https://randomuser.me/api/portraits/women/10.jpg",
42
+ },
43
+ {
44
+ id: "14",
45
+ container: "To Do",
46
+ title: "Task 4",
47
+ description: "Add jest tests",
48
+ assignee_name: "Mike James",
49
+ assignee_img: "https://randomuser.me/api/portraits/men/8.jpg",
50
+ },
51
+ {
52
+ id: "15",
53
+ container: "Done",
54
+ title: "Task 5",
55
+ description: "Alpha testing",
56
+ assignee_name: "James Guy",
57
+ assignee_img: "https://randomuser.me/api/portraits/men/18.jpg",
58
+ },
59
+ {
60
+ id: "16",
61
+ container: "In Progress",
62
+ title: "Task 6",
63
+ description: "Release",
64
+ assignee_name: "Sally Jones",
65
+ assignee_img: "https://randomuser.me/api/portraits/women/28.jpg",
66
+ },
67
+ ];
68
+
69
+ const DraggableMultipleContainersDropzone = (props) => {
70
+ const [initialState, setInitialState] = useState(data);
71
+
72
+ const badgeProperties = (container) => {
73
+ switch (container) {
74
+ case "To Do":
75
+ return { text: "queue", color: "warning" };
76
+ case "In Progress":
77
+ return { text: "progress", color: "primary" };
78
+ default:
79
+ return { text: "done", color: "success" };
80
+ }
81
+ };
82
+
83
+ return (
84
+ <DraggableProvider
85
+ dropZone={{type: "outline"}}
86
+ enableCrossContainerPreview
87
+ initialItems={data}
88
+ onDragEnd={(draggedItemId, finalContainer, originalContainer, itemAbove, itemBelow) => {
89
+ console.log(`Dragged Item ID: ${draggedItemId}`);
90
+ console.log(`Final Container: ${finalContainer}`);
91
+ console.log(`Original Container: ${originalContainer}`);
92
+ console.log('Item Above:', itemAbove);
93
+ console.log('Item Below:', itemBelow);
94
+ }}
95
+ onDrop={(draggedItemId, droppedContainer, originalContainer, item, itemAbove, itemBelow) => {
96
+ console.log(`Dragged Item ID: ${draggedItemId}`);
97
+ console.log(`Dropped Container: ${droppedContainer}`);
98
+ console.log(`Original Container: ${originalContainer}`);
99
+ console.log('Dropped Item:', item);
100
+ console.log('Item Above:', itemAbove);
101
+ console.log('Item Below:', itemBelow);
102
+ }}
103
+ onReorder={(items) => setInitialState(items)}
104
+ >
105
+ <Flex
106
+ justifyContent="center"
107
+ {...props}
108
+ >
109
+ {containers?.map((container) => (
110
+ <Draggable.Container
111
+ container={container}
112
+ htmlOptions={{style:{ width: "200px", height: "70vh"}}}
113
+ key={container}
114
+ padding="sm"
115
+ >
116
+ <Caption textAlign="center">{container}</Caption>
117
+ <Flex
118
+ alignItems="stretch"
119
+ gap="sm"
120
+ orientation="column"
121
+ >
122
+ {initialState
123
+ .filter((item) => item.container === container)
124
+ .map(
125
+ ({
126
+ assignee_img,
127
+ assignee_name,
128
+ description,
129
+ id,
130
+ title,
131
+ }) => (
132
+ <Draggable.Item
133
+ container={container}
134
+ dragId={id}
135
+ key={id}
136
+ >
137
+ <Card
138
+ padding="sm"
139
+ {...props}
140
+ >
141
+ <Flex justify="between">
142
+ <FlexItem>
143
+ <Flex>
144
+ <Avatar
145
+ imageUrl={assignee_img}
146
+ name={assignee_name}
147
+ size="xxs"
148
+ />
149
+ <Title paddingLeft="xs"
150
+ size={4}
151
+ text={title}
152
+ {...props}
153
+ />
154
+ </Flex>
155
+ </FlexItem>
156
+ <Badge
157
+ marginLeft="sm"
158
+ rounded
159
+ text={badgeProperties(container).text}
160
+ variant={badgeProperties(container).color}
161
+ {...props}
162
+ />
163
+ </Flex>
164
+ <Body paddingTop="xs"
165
+ text={description}
166
+ {...props}
167
+ />
168
+ </Card>
169
+ </Draggable.Item>
170
+ )
171
+ )}
172
+ </Flex>
173
+ </Draggable.Container>
174
+ ))}
175
+ </Flex>
176
+ </DraggableProvider>
177
+ );
178
+ };
179
+
180
+ export default DraggableMultipleContainersDropzone;