From abc1d0f1ff5fb9ae53e14f5b970093e8c322e6ea Mon Sep 17 00:00:00 2001 From: kmcfaul <45077788+kmcfaul@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:57:34 -0400 Subject: [PATCH] V6 - feat(DragDrop): add multiple drop zone support (#10771) * feat(DragDrop next): add multiple drop zone support (#10614) * feat(DragDrop-next): add multiple drop zone support * update dupe ids * pass 2 - fixed collision bugs and ref passthrough * refactor DragDropSort to use DragDropContainer * move DraggableObject to Container * updated import, remove unused context * remove green styling for dragover * fix overlay styling, glob import * lock * revert glob * update type * fix doubled directory and merge changes into base * lock * fix var/tokens, remove double example, fix DLS context import * fix variant descriptions, remove TableComposable variant * snap --- .../src/components/DataList/DataList.tsx | 118 +++---- .../DualListSelector/DualListSelectorList.tsx | 14 +- .../components/DragDrop/DragDropContainer.tsx | 305 ++++++++++++++++++ .../src/components/DragDrop/DragDropSort.tsx | 159 ++------- .../DraggableDualListSelectorListItem.tsx | 4 +- .../src/components/DragDrop/Droppable.tsx | 79 ++++- .../components/DragDrop/DroppableContext.ts | 6 - .../DragDrop/__tests__/DragDrop.test.tsx | 5 + .../DragDrop/examples/BasicSorting.tsx | 0 .../examples/BasicSortingWithDragButton.tsx | 19 -- .../DragDrop/examples/DataListDraggable.tsx | 1 + .../components/DragDrop/examples/DragDrop.md | 10 +- .../examples/DragDropContainerBasic.tsx | 45 +++ .../examples/DragDropContainerDataList.tsx | 75 +++++ .../DragDropContainerDualListSelector.tsx | 191 +++++++++++ .../DragDrop/examples/DragDropDemos.md | 26 +- .../src/components/DragDrop/index.ts | 2 + .../next/components/DragDrop/DragButton.tsx | 27 -- .../next/components/DragDrop/DragDropSort.tsx | 186 ----------- .../next/components/DragDrop/Draggable.tsx | 58 ---- .../DragDrop/DraggableDataListItem.tsx | 61 ---- .../DraggableDualListSelectorListItem.tsx | 93 ------ .../next/components/DragDrop/Droppable.tsx | 27 -- .../components/DragDrop/DroppableContext.ts | 6 - .../DragDrop/__tests__/DragDrop.test.tsx | 24 -- .../__snapshots__/DragDrop.test.tsx.snap | 59 ---- .../examples/BasicSortingWithDragButton.tsx | 20 -- .../DragDrop/examples/DataListDraggable.tsx | 38 --- .../components/DragDrop/examples/DragDrop.md | 32 -- .../DragDrop/examples/DragDropDemos.md | 37 --- .../examples/DualListSelectorDraggable.tsx | 150 --------- .../src/next/components/DragDrop/index.ts | 1 - .../src/next/components/index.ts | 1 - packages/react-drag-drop/src/next/index.ts | 1 - 34 files changed, 811 insertions(+), 1069 deletions(-) create mode 100644 packages/react-drag-drop/src/components/DragDrop/DragDropContainer.tsx delete mode 100644 packages/react-drag-drop/src/components/DragDrop/DroppableContext.ts rename packages/react-drag-drop/src/{next => }/components/DragDrop/examples/BasicSorting.tsx (100%) delete mode 100644 packages/react-drag-drop/src/components/DragDrop/examples/BasicSortingWithDragButton.tsx create mode 100644 packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerBasic.tsx create mode 100644 packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerDataList.tsx create mode 100644 packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerDualListSelector.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/DragButton.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/DragDropSort.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/Draggable.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/DraggableDataListItem.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/DroppableContext.ts delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/__tests__/DragDrop.test.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/__tests__/__snapshots__/DragDrop.test.tsx.snap delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSortingWithDragButton.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/examples/DataListDraggable.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/examples/DragDrop.md delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropDemos.md delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/examples/DualListSelectorDraggable.tsx delete mode 100644 packages/react-drag-drop/src/next/components/DragDrop/index.ts delete mode 100644 packages/react-drag-drop/src/next/components/index.ts delete mode 100644 packages/react-drag-drop/src/next/index.ts diff --git a/packages/react-core/src/components/DataList/DataList.tsx b/packages/react-core/src/components/DataList/DataList.tsx index 817fb1c6875..cc9857baba4 100644 --- a/packages/react-core/src/components/DataList/DataList.tsx +++ b/packages/react-core/src/components/DataList/DataList.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/DataList/data-list'; -import { PickOptional } from '../../helpers/typeUtils'; const gridBreakpointClasses = { none: styles.modifiers.gridNone, @@ -19,7 +18,7 @@ export enum DataListWrapModifier { breakWord = 'breakWord' } -export interface DataListProps extends Omit, 'ref'> { +export interface DataListProps extends React.HTMLProps { /** Content rendered inside the DataList list */ children?: React.ReactNode; /** Additional classes added to the DataList list */ @@ -38,6 +37,8 @@ export interface DataListProps extends Omit, ' wrapModifier?: DataListWrapModifier | 'nowrap' | 'truncate' | 'breakWord'; /** Object that causes the data list to render hidden inputs which improve selectable item a11y */ onSelectableRowChange?: (event: React.FormEvent, id: string) => void; + /** @hide custom ref of the DataList */ + innerRef?: React.RefObject; } interface DataListContextProps { @@ -51,71 +52,58 @@ export const DataListContext = React.createContext isSelectable: false }); -class DataList extends React.Component { - static displayName = 'DataList'; - static defaultProps: PickOptional = { - children: null, - className: '', - selectedDataListItemId: '', - isCompact: false, - gridBreakpoint: 'md', - wrapModifier: null - }; - ref = React.createRef(); - - constructor(props: DataListProps) { - super(props); - } +export const DataListBase: React.FunctionComponent = ({ + children = null, + className = '', + 'aria-label': ariaLabel, + onSelectDataListItem, + selectedDataListItemId = '', + isCompact = false, + gridBreakpoint = 'md', + wrapModifier = null, + onSelectableRowChange, + innerRef, + ...props +}: DataListProps) => { + const isSelectable = onSelectDataListItem !== undefined; - getIndex = (id: string) => Array.from(this.ref.current.children).findIndex((item) => item.id === id); + const updateSelectedDataListItem = (event: React.MouseEvent | React.KeyboardEvent, id: string) => { + onSelectDataListItem(event, id); + }; - render() { - const { - className, - children, - 'aria-label': ariaLabel, - onSelectDataListItem, - selectedDataListItemId, - isCompact, - wrapModifier, - gridBreakpoint, - onSelectableRowChange, - ...props - } = this.props; - const isSelectable = onSelectDataListItem !== undefined; + return ( + +
    + {children} +
+
+ ); +}; - const updateSelectedDataListItem = (event: React.MouseEvent | React.KeyboardEvent, id: string) => { - onSelectDataListItem(event, id); - }; +DataListBase.displayName = 'DataListBase'; - return ( - -
    - {children} -
-
- ); - } -} +export const DataList = React.forwardRef((props: DataListProps, ref: React.Ref) => ( + } {...props} /> +)); -export { DataList }; +DataList.displayName = 'DataList'; diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorList.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorList.tsx index 13d00467e6b..c339870ade3 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorList.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorList.tsx @@ -9,10 +9,13 @@ import { DualListSelectorListContext } from './DualListSelectorContext'; export interface DualListSelectorListProps extends React.HTMLProps { /** Content rendered inside the dual list selector list. */ children?: React.ReactNode; + /** @hide forwarded ref */ + innerRef?: React.RefObject; } -export const DualListSelectorList: React.FunctionComponent = ({ +export const DualListSelectorListBase: React.FunctionComponent = ({ children, + innerRef, ...props }: DualListSelectorListProps) => { const { isTree, ariaLabelledBy, focusedOption, displayOption, selectedOptions, id, options, isDisabled } = @@ -31,6 +34,7 @@ export const DualListSelectorList: React.FunctionComponent {options.length === 0 @@ -54,4 +58,12 @@ export const DualListSelectorList: React.FunctionComponent ); }; +DualListSelectorListBase.displayName = 'DualListSelectorListBase'; + +export const DualListSelectorList = React.forwardRef( + (props: DualListSelectorListProps, ref: React.Ref) => ( + } {...props} /> + ) +); + DualListSelectorList.displayName = 'DualListSelectorList'; diff --git a/packages/react-drag-drop/src/components/DragDrop/DragDropContainer.tsx b/packages/react-drag-drop/src/components/DragDrop/DragDropContainer.tsx new file mode 100644 index 00000000000..323ef5a689e --- /dev/null +++ b/packages/react-drag-drop/src/components/DragDrop/DragDropContainer.tsx @@ -0,0 +1,305 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { css } from '@patternfly/react-styles'; +import { + DndContext, + closestCenter, + DragOverlay, + DndContextProps, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, + DragStartEvent, + UniqueIdentifier, + DragOverEvent, + CollisionDetection, + pointerWithin, + rectIntersection, + getFirstCollision, + DragCancelEvent +} from '@dnd-kit/core'; +import { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'; +import { Draggable } from './Draggable'; +import { DraggableDataListItem } from './DraggableDataListItem'; +import { DraggableDualListSelectorListItem } from './DraggableDualListSelectorListItem'; +import styles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; +import { DataList, canUseDOM } from '@patternfly/react-core'; + +export type DragDropContainerDragStartEvent = DragStartEvent; +export type DragDropContainerDragOverEvent = DragOverEvent; +export type DragDropContainerDragEndEvent = DragEndEvent; +export type DragDropContainerDragCancelEvent = DragCancelEvent; + +export interface DraggableObject { + /** Unique id of the draggable object */ + id: string | number; + /** Content rendered in the draggable object */ + content: React.ReactNode; + /** Props spread to the rendered wrapper of the draggable object */ + props?: any; +} + +/** + * DragDropSortProps extends dnd-kit's props which may be viewed at https://docs.dndkit.com/api-documentation/context-provider#props. + */ +export interface DragDropContainerProps extends DndContextProps { + /** Content containing one or more Droppable zones. */ + children?: React.ReactNode; + /** Set of records of all child droppables - their zone IDs and their draggable items. */ + items: Record; + /** Callback when use begins dragging a draggable object */ + onDrag?: (event: DragDropContainerDragStartEvent) => void; + /** Callback when an item is dragged to another container */ + onContainerMove?: (event: DragDropContainerDragOverEvent, items: Record) => void; + /** Callback when user drops a draggable object */ + onDrop: (event: DragDropContainerDragEndEvent, items: Record) => void; + /** Callback when drag is cancelled */ + onCancel?: (event: DragDropContainerDragCancelEvent, items: Record) => void; + /** The variant determines which component wraps the draggable object. + * Default variant wraps the draggable object in a div. + * DataList variant wraps the draggable object in a DataListItem + * DualListSelectorList variant wraps the draggable objects in a DualListSelectorListItem and a div.pf-c-dual-list-selector__item-text element + * */ + variant?: 'default' | 'DataList' | 'DualListSelectorList'; + /** Additional classes to apply to the drag overlay */ + overlayProps?: any; +} + +export const DragDropContainer: React.FunctionComponent = ({ + children, + items, + onDrag = () => {}, + onContainerMove = () => {}, + onDrop = () => {}, + onCancel = () => {}, + variant = 'default', + overlayProps, + ...props +}: DragDropContainerProps) => { + const itemsCopy = React.useRef | null>(null); + const hasRecentlyMovedContainer = React.useRef(false); + const [activeId, setActiveId] = React.useState(null); + const lastOverId = React.useRef(null); + + const findItem = React.useCallback( + (id: UniqueIdentifier, containerId: UniqueIdentifier) => items[containerId].find((item) => item.id === id), + [items] + ); + + const findContainer = React.useCallback( + (id: UniqueIdentifier) => { + if (id in items) { + return id; + } + return Object.keys(items).find((key) => items[key].find((obj) => obj.id === id)); + }, + [items] + ); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates + }) + ); + + const collisionDetectionStrategy: CollisionDetection = React.useCallback( + (args) => { + if (activeId && activeId in items) { + return closestCenter({ + ...args, + droppableContainers: args.droppableContainers.filter((container) => container.id in items) + }); + } + + const pointerIntersections = pointerWithin(args); + const intersections = pointerIntersections.length > 0 ? pointerIntersections : rectIntersection(args); + let overId = getFirstCollision(intersections, 'id'); + + if (overId != null) { + if (overId in items) { + const containerItems = items[overId]; + + if (containerItems.length > 0) { + overId = closestCenter({ + ...args, + droppableContainers: args.droppableContainers.filter( + (container) => container.id !== overId && containerItems.find((obj) => obj.id === container.id) + ) + })[0]?.id; + } + } + + lastOverId.current = overId; + + return [{ id: overId }]; + } + + if (hasRecentlyMovedContainer.current) { + lastOverId.current = activeId; + } + return lastOverId.current ? [{ id: lastOverId.current }] : []; + }, + [activeId, items] + ); + + React.useEffect(() => { + requestAnimationFrame(() => { + hasRecentlyMovedContainer.current = false; + }); + }, [items]); + + const handleDragStart = (event: DragStartEvent) => { + const { active } = event; + itemsCopy.current = { ...items }; + setActiveId(active.id); + + onDrag(event); + }; + + const handleDragOver = (event: DragOverEvent) => { + const { active, over } = event; + const { id: activeId } = active; + const { id: overId } = over; + + if (!overId || activeId in items) { + return; + } + + const activeContainer = findContainer(activeId); + const overContainer = findContainer(overId); + + if (!overContainer || !activeContainer) { + return; + } + + if (activeContainer !== overContainer) { + const activeItems = items[activeContainer]; + const overItems = items[overContainer]; + const overIndex = overItems.findIndex((draggableItem) => draggableItem.id === overId); + const activeIndex = activeItems.findIndex((draggableItem) => draggableItem.id === activeId); + + const isBelowOverItem = + over && active.rect.current.translated && active.rect.current.translated.top > over.rect.top + over.rect.height; + + const modifier = isBelowOverItem ? 1 : 0; + const newIndex = overIndex >= 0 ? overIndex + modifier : overItems.length + 1; + + const newItems = { + ...items, + [activeContainer]: items[activeContainer].filter((item) => item.id !== active.id), + [overContainer]: [ + ...items[overContainer].slice(0, newIndex), + items[activeContainer][activeIndex], + ...items[overContainer].slice(newIndex, items[overContainer].length) + ] + }; + + hasRecentlyMovedContainer.current = true; + onContainerMove(event, newItems); + } + }; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + const { id: activeId } = active; + const { id: overId } = over; + + const activeContainer = findContainer(activeId); + if (!over || !activeContainer) { + setActiveId(null); + return; + } + + const overContainer = findContainer(overId); + if (!overContainer) { + setActiveId(null); + return; + } + + const activeIndex = items[activeContainer].findIndex((draggableItem) => draggableItem.id === activeId); + const overIndex = items[overContainer].findIndex((draggableItem) => draggableItem.id === overId); + + if (activeIndex !== overIndex) { + const newItems = { ...items, [overContainer]: arrayMove(items[overContainer], activeIndex, overIndex) }; + onDrop(event, newItems); + } + setActiveId(null); + }; + + const handleDragCancel = (event: DragCancelEvent) => { + onCancel(event, itemsCopy.current); + itemsCopy.current = null; + setActiveId(null); + }; + + const getDragOverlay = () => { + if (!activeId) { + return; + } + const item = findItem(activeId, findContainer(activeId)); + + let content; + switch (variant) { + case 'DualListSelectorList': + content = ( + + {item.content} + + ); + break; + case 'DataList': + content = ( + + {item.content} + + ); + break; + default: + content = ( + + {item.content} + + ); + } + + return ( +
+ {variant === 'DualListSelectorList' &&
    {content}
} + {variant === 'DataList' && ( + + {content} + + )} + {variant !== 'DualListSelectorList' && variant !== 'DataList' && content} +
+ ); + }; + + const dragOverlay = {activeId && getDragOverlay()}; + return ( + + {children} + {canUseDOM ? ReactDOM.createPortal(dragOverlay, document.getElementById('root')) : dragOverlay} + + ); +}; +DragDropContainer.displayName = 'DragDropContainer'; diff --git a/packages/react-drag-drop/src/components/DragDrop/DragDropSort.tsx b/packages/react-drag-drop/src/components/DragDrop/DragDropSort.tsx index faa64957d8c..1a1709f1a40 100644 --- a/packages/react-drag-drop/src/components/DragDrop/DragDropSort.tsx +++ b/packages/react-drag-drop/src/components/DragDrop/DragDropSort.tsx @@ -1,40 +1,11 @@ import * as React from 'react'; -import { css } from '@patternfly/react-styles'; -import { - DndContext, - closestCenter, - DragOverlay, - DndContextProps, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - DragEndEvent, - DragStartEvent -} from '@dnd-kit/core'; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - verticalListSortingStrategy -} from '@dnd-kit/sortable'; -import { Draggable } from './Draggable'; -import { DraggableDataListItem } from './DraggableDataListItem'; -import { DraggableDualListSelectorListItem } from './DraggableDualListSelectorListItem'; -import styles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; +import { DndContextProps, DragEndEvent, DragStartEvent } from '@dnd-kit/core'; +import { Droppable } from './Droppable'; +import { DragDropContainer, DraggableObject } from './DragDropContainer'; export type DragDropSortDragEndEvent = DragEndEvent; export type DragDropSortDragStartEvent = DragStartEvent; -export interface DraggableObject { - /** Unique id of the draggable object */ - id: string; - /** Content rendered in the draggable object */ - content: React.ReactNode; - /** Props spread to the rendered wrapper of the draggable object */ - props?: any; -} - /** * DragDropSortProps extends dnd-kit's props which may be viewed at https://docs.dndkit.com/api-documentation/context-provider#props. */ @@ -50,11 +21,12 @@ export interface DragDropSortProps extends DndContextProps { onDrag?: (event: DragDropSortDragStartEvent, oldIndex: number) => void; /** The variant determines which component wraps the draggable object. * Default variant wraps the draggable object in a div. - * DataList vairant wraps the draggable object in a DataListItem + * DataList variant wraps the draggable object in a DataListItem * DualListSelectorList variant wraps the draggable objects in a DualListSelectorListItem and a div.pf-c-dual-list-selector__item-text element - * TableComposable variant wraps the draggable objects in TODO * */ - variant?: 'default' | 'DataList' | 'DualListSelectorList' | 'TableComposable'; + variant?: 'default' | 'DataList' | 'DualListSelectorList'; + /** Additional classes to apply to the drag overlay */ + overlayProps?: any; } export const DragDropSort: React.FunctionComponent = ({ @@ -63,120 +35,33 @@ export const DragDropSort: React.FunctionComponent = ({ onDrag = () => {}, variant = 'default', children, + overlayProps, ...props }: DragDropSortProps) => { - const [activeId, setActiveId] = React.useState(null); const itemIds = React.useMemo(() => (items ? Array.from(items, (item) => item.id as string) : []), [items]); - const getItemById = (id: string): DraggableObject => items.find((item) => item.id === id); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates - }) - ); - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - const oldIndex = itemIds.indexOf(active.id as string); - const newIndex = itemIds.indexOf(over.id as string); - const newItems = arrayMove(items, oldIndex, newIndex); - onDrop(event, newItems, oldIndex, newIndex); - setActiveId(null); - }; - const handleDragStart = (event: DragStartEvent) => { - setActiveId(event.active.id as string); onDrag(event, itemIds.indexOf(event.active.id as string)); }; - const getDragOverlay = () => { - if (!activeId) { - return; - } - const item = getItemById(activeId); - - let content; - switch (variant) { - case 'DualListSelectorList': - content = ( - - {item.content} - - ); - break; - case 'DataList': - content = ( - - {item.content} - - ); - break; - default: - content = ( - - {item.content} - - ); - } - - return ( -
- {content} -
- ); + const handleDragEnd = (event: DragEndEvent, newItems: Record) => { + const { active, over } = event; + const oldIndex = itemIds.indexOf(active.id as string); + const newIndex = itemIds.indexOf(over.id as string); + onDrop(event, newItems[dropZoneId], oldIndex, newIndex); }; - const renderedChildren = ( - - {items.map((item: DraggableObject) => { - switch (variant) { - case 'DualListSelectorList': - return ( - - {item.content} - - ); - case 'DataList': - return ( - - {item.content} - - ); - default: - return ( - - {item.content} - - ); - } - })} - {activeId && getDragOverlay()} - - ); - + const dropZoneId = props.id ? props.id : 'droppable'; return ( - - {children && - React.cloneElement(children, { - children: renderedChildren - })} - {!children &&
{renderedChildren}
} -
+ + ); }; DragDropSort.displayName = 'DragDropSort'; diff --git a/packages/react-drag-drop/src/components/DragDrop/DraggableDualListSelectorListItem.tsx b/packages/react-drag-drop/src/components/DragDrop/DraggableDualListSelectorListItem.tsx index eef43cf82c3..15ced83f48d 100644 --- a/packages/react-drag-drop/src/components/DragDrop/DraggableDualListSelectorListItem.tsx +++ b/packages/react-drag-drop/src/components/DragDrop/DraggableDualListSelectorListItem.tsx @@ -5,7 +5,7 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; import dragStyles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; import { DragButton } from './DragButton'; -import { DualListSelectorListContext as DLSListContextDeprecated } from '@patternfly/react-core/dist/esm/deprecated/components/DualListSelector'; +import { DualListSelectorListContext } from '@patternfly/react-core/dist/esm/components/DualListSelector/DualListSelectorContext'; export interface DraggableDualListSelectorListItemProps extends React.HTMLProps { /** Content rendered inside DragDrop */ @@ -42,7 +42,7 @@ export const DraggableDualListSelectorListItem: React.FunctionComponent false }); - const { setFocusedOption } = React.useContext(DLSListContextDeprecated); + const { setFocusedOption } = React.useContext(DualListSelectorListContext); const style = { transform: CSS.Transform.toString(transform), diff --git a/packages/react-drag-drop/src/components/DragDrop/Droppable.tsx b/packages/react-drag-drop/src/components/DragDrop/Droppable.tsx index ccb05fe9e26..45106fb46a6 100644 --- a/packages/react-drag-drop/src/components/DragDrop/Droppable.tsx +++ b/packages/react-drag-drop/src/components/DragDrop/Droppable.tsx @@ -1,27 +1,76 @@ import * as React from 'react'; import { useDroppable } from '@dnd-kit/core'; +import { DraggableObject } from './DragDropContainer'; +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { DraggableDualListSelectorListItem } from './DraggableDualListSelectorListItem'; +import { DraggableDataListItem } from './DraggableDataListItem'; +import { Draggable } from './Draggable'; interface DroppableProps extends React.HTMLProps { - /** Content rendered inside DragDrop */ - children?: React.ReactNode; - /** Class to add to outer div */ + /** ID of the drop zone */ + id?: string; + /** Additional classes added to the div, or cloned element if wrapper is used */ className?: string; - /** Name of zone that items can be dragged between. Should specify if there is more than one Droppable on the page. */ - zone?: string; - /** Id to be passed back on drop events */ - droppableId?: string; - /** Don't wrap the component in a div. Requires passing a single child. */ - hasNoWrapper?: boolean; + /** Array of draggable objects */ + items: DraggableObject[]; + /** Alternative to wrapping drop zone in a div, will override any ref and style set on the element. */ + wrapper?: React.ReactElement; + /** The variant determines which component wraps the draggable object. + * Default variant wraps the draggable object in a div. + * DataList vairant wraps the draggable object in a DataListItem + * DualListSelectorList variant wraps the draggable objects in a DualListSelectorListItem and a div.pf-c-dual-list-selector__item-text element + * TableComposable variant wraps the draggable objects in TODO + * */ + variant?: 'default' | 'DataList' | 'DualListSelectorList' | 'TableComposable'; } -export const Droppable: React.FunctionComponent = ({ children, ...props }: DroppableProps) => { - const { isOver, setNodeRef } = useDroppable({ id: 'droppable' }); - const style = { color: isOver ? 'green' : undefined }; +export const Droppable: React.FunctionComponent = ({ + items, + id = 'droppable', + variant = 'default', + wrapper, + ...props +}: DroppableProps) => { + const itemIds = React.useMemo(() => (items ? Array.from(items, (item) => item.id as string) : []), [items]); + const { setNodeRef } = useDroppable({ id: id ? id : 'droppable' }); + + const content = items.map((item: DraggableObject) => { + switch (variant) { + case 'DualListSelectorList': + return ( + + {item.content} + + ); + case 'DataList': + return ( + + {item.content} + + ); + default: + return ( + + {item.content} + + ); + } + }); return ( -
- {children} -
+ + {wrapper && + React.cloneElement(wrapper, { + children: content, + ref: setNodeRef, + ...props + })} + {!wrapper && ( +
+ {content} +
+ )} +
); }; Droppable.displayName = 'Droppable'; diff --git a/packages/react-drag-drop/src/components/DragDrop/DroppableContext.ts b/packages/react-drag-drop/src/components/DragDrop/DroppableContext.ts deleted file mode 100644 index 888cb4fc66f..00000000000 --- a/packages/react-drag-drop/src/components/DragDrop/DroppableContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from 'react'; - -export const DroppableContext = React.createContext({ - zone: 'defaultDroppableZone', - droppableId: 'defaultDroppableId' -}); diff --git a/packages/react-drag-drop/src/components/DragDrop/__tests__/DragDrop.test.tsx b/packages/react-drag-drop/src/components/DragDrop/__tests__/DragDrop.test.tsx index e8831b934a4..98a08bf4c91 100644 --- a/packages/react-drag-drop/src/components/DragDrop/__tests__/DragDrop.test.tsx +++ b/packages/react-drag-drop/src/components/DragDrop/__tests__/DragDrop.test.tsx @@ -2,6 +2,11 @@ import React from 'react'; import { render } from '@testing-library/react'; import { DragDropSort } from '../'; +jest.mock('react-dom', () => ({ + ...jest.requireActual('react-dom'), + createPortal: jest.fn((node) => node) +})); + test('renders some divs', () => { const { asFragment } = render(
diff --git a/packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSorting.tsx b/packages/react-drag-drop/src/components/DragDrop/examples/BasicSorting.tsx similarity index 100% rename from packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSorting.tsx rename to packages/react-drag-drop/src/components/DragDrop/examples/BasicSorting.tsx diff --git a/packages/react-drag-drop/src/components/DragDrop/examples/BasicSortingWithDragButton.tsx b/packages/react-drag-drop/src/components/DragDrop/examples/BasicSortingWithDragButton.tsx deleted file mode 100644 index 1988442db7e..00000000000 --- a/packages/react-drag-drop/src/components/DragDrop/examples/BasicSortingWithDragButton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { DragDropSort, DraggableObject } from '@patternfly/react-drag-drop'; - -export const BasicSortingWithDragButton: React.FunctionComponent = () => { - const [items, setItems] = React.useState([ - { id: 'with-button-1', content: 'one' }, - { id: 'with-button-2', content: 'two' }, - { id: 'with-button-3', content: 'three' } - ]); - - return ( - { - setItems(newItems); - }} - /> - ); -}; diff --git a/packages/react-drag-drop/src/components/DragDrop/examples/DataListDraggable.tsx b/packages/react-drag-drop/src/components/DragDrop/examples/DataListDraggable.tsx index ce80773061e..d1b4bf7d6a8 100644 --- a/packages/react-drag-drop/src/components/DragDrop/examples/DataListDraggable.tsx +++ b/packages/react-drag-drop/src/components/DragDrop/examples/DataListDraggable.tsx @@ -31,6 +31,7 @@ export const DataListDraggable: React.FunctionComponent = (props) => { setItems(newItems); }} variant="DataList" + overlayProps={{ isCompact: true }} > diff --git a/packages/react-drag-drop/src/components/DragDrop/examples/DragDrop.md b/packages/react-drag-drop/src/components/DragDrop/examples/DragDrop.md index 2cf2960cb4d..0ce5e7e8e51 100644 --- a/packages/react-drag-drop/src/components/DragDrop/examples/DragDrop.md +++ b/packages/react-drag-drop/src/components/DragDrop/examples/DragDrop.md @@ -15,12 +15,18 @@ import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-d import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon'; -import { DragDropSort } from '@patternfly/react-drag-drop'; +import { DragDropSort, DragDropContainer, Droppable as NewDroppable } from '@patternfly/react-drag-drop'; ## Sorting examples ### Basic drag and drop sorting -```ts file="./BasicSortingWithDragButton.tsx" +```ts file="./BasicSorting.tsx" + +``` + +### Multiple drop zones + +```ts file="./DragDropContainerBasic.tsx" ``` diff --git a/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerBasic.tsx b/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerBasic.tsx new file mode 100644 index 00000000000..cd0e98f95f4 --- /dev/null +++ b/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerBasic.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { + Droppable as NewDroppable, + DraggableObject, + DragDropContainer, + DragDropContainerDragOverEvent, + DragDropContainerDragEndEvent, + DragDropContainerDragCancelEvent +} from '@patternfly/react-drag-drop'; + +export const DragDropContainerBasic: React.FunctionComponent = () => { + const [allItems, setAllItems] = React.useState>({ + container1: [ + { id: 'button-1', content: 'one' }, + { id: 'button-2', content: 'two' }, + { id: 'button-3', content: 'three' } + ], + container2: [ + { id: 'button-4', content: 'four' }, + { id: 'button-5', content: 'five' }, + { id: 'button-6', content: 'six' } + ] + }); + + return ( + ) => { + setAllItems(newItems); + }} + onContainerMove={(_event: DragDropContainerDragOverEvent, newItems: Record) => { + setAllItems(newItems); + }} + onCancel={(_event: DragDropContainerDragCancelEvent, prevItems: Record) => { + setAllItems(prevItems); + }} + > +

group 1

+ +
+

group 2

+ +
+ ); +}; diff --git a/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerDataList.tsx b/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerDataList.tsx new file mode 100644 index 00000000000..cd9a0cc7836 --- /dev/null +++ b/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerDataList.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { + DataList, + DataListCell, + DataListCheck, + DataListControl, + DataListItemCells, + Grid, + GridItem +} from '@patternfly/react-core'; +import { DragDropContainer, DraggableObject, Droppable as NewDroppable } from '@patternfly/react-drag-drop'; + +const getItems = (from: number, count: number): DraggableObject[] => + Array.from({ length: count }, (_, idx) => from + idx).map((idx) => ({ + id: `data-list-item-${idx}`, + content: ( + <> + + + + + {`item-${idx}`} + + ]} + /> + + ) + })); + +export const DataListDraggable: React.FunctionComponent = (props) => { + const [items, setItems] = React.useState>({ + group1: getItems(0, 5), + group2: getItems(5, 5) + }); + + return ( + { + setItems(newItems); + }} + onContainerMove={(_, newItems) => { + setItems(newItems); + }} + onCancel={(_, prevItems) => { + setItems(prevItems); + }} + variant="DataList" + overlayProps={{ isCompact: true }} + > + + +

group 1

+ } + /> +
+ +

group 2

+ } + /> +
+
+
+ ); +}; diff --git a/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerDualListSelector.tsx b/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerDualListSelector.tsx new file mode 100644 index 00000000000..ea058fdb452 --- /dev/null +++ b/packages/react-drag-drop/src/components/DragDrop/examples/DragDropContainerDualListSelector.tsx @@ -0,0 +1,191 @@ +import React from 'react'; +import { + DualListSelector, + DualListSelectorPane, + DualListSelectorList, + DualListSelectorControlsWrapper, + DualListSelectorControl +} from '@patternfly/react-core'; +import { DragDropContainer, DraggableObject, Droppable as NewDroppable } from '@patternfly/react-drag-drop'; + +import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon'; +import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon'; +import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon'; +import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; + +export const DragDropContainerDualListSelector: React.FunctionComponent = () => { + const [ignoreNextOptionSelect, setIgnoreNextOptionSelect] = React.useState(false); + const [availableOptions, setAvailableOptions] = React.useState([ + { id: 'Apple', content: 'Apple', props: { key: 'Apple', isSelected: false } }, + { id: 'Banana', content: 'Banana', props: { key: 'Banana', isSelected: false } }, + { id: 'Pineapple', content: 'Pineapple', props: { key: 'Pineapple', isSelected: false } } + ]); + + const [chosenOptions, setChosenOptions] = React.useState([ + { id: 'Orange', content: 'Orange', props: { key: 'Orange', isSelected: false } }, + { id: 'Grape', content: 'Grape', props: { key: 'Grape', isSelected: false } }, + { id: 'Peach', content: 'Peach', props: { key: 'Peach', isSelected: false } }, + { id: 'Strawberry', content: 'Strawberry', props: { key: 'Strawberry', isSelected: false } } + ]); + + const [allDraggableItems, setAllItems] = React.useState>({ + available: availableOptions.map((option, index) => ({ + ...option, + props: { + key: option.props.key, + isSelected: option.props.isSelected, + onOptionSelect: (e) => onOptionSelect(e, index, false) + } + })), + chosen: chosenOptions.map((option, index) => ({ + ...option, + props: { + key: option.props.key, + isSelected: option.props.isSelected, + onOptionSelect: (e) => onOptionSelect(e, index, true) + } + })) + }); + + const handleDragOperation = (_event: any, items: Record) => { + setAvailableOptions(items.available); + setChosenOptions(items.chosen); + setAllItems(items); + }; + + React.useEffect(() => { + setAllItems({ + available: availableOptions.map((option, index) => ({ + ...option, + props: { + key: option.props.key, + isSelected: option.props.isSelected, + onOptionSelect: (e) => onOptionSelect(e, index, false) + } + })), + chosen: chosenOptions.map((option, index) => ({ + ...option, + props: { + key: option.props.key, + isSelected: option.props.isSelected, + onOptionSelect: (e) => onOptionSelect(e, index, true) + } + })) + }); + }, [availableOptions, chosenOptions]); + + const moveSelected = (fromAvailable) => { + const sourceOptions = fromAvailable ? availableOptions : chosenOptions; + const destinationOptions = fromAvailable ? chosenOptions : availableOptions; + for (let i = 0; i < sourceOptions.length; i++) { + const option = sourceOptions[i]; + if (option.props.isSelected) { + sourceOptions.splice(i, 1); + destinationOptions.push(option); + option.props.isSelected = false; + i--; + } + } + if (fromAvailable) { + setAvailableOptions([...sourceOptions]); + setChosenOptions([...destinationOptions]); + } else { + setChosenOptions([...sourceOptions]); + setAvailableOptions([...destinationOptions]); + } + }; + + const moveAll = (fromAvailable) => { + if (fromAvailable) { + setChosenOptions([...availableOptions, ...chosenOptions]); + setAvailableOptions([]); + } else { + setAvailableOptions([...chosenOptions, ...availableOptions]); + setChosenOptions([]); + } + }; + + const onOptionSelect = (event, index, isChosen) => { + if (ignoreNextOptionSelect) { + setIgnoreNextOptionSelect(false); + return; + } + if (isChosen) { + const newChosen = [...chosenOptions]; + newChosen[index].props.isSelected = !chosenOptions[index].props.isSelected; + setChosenOptions(newChosen); + } else { + const newAvailable = [...availableOptions]; + newAvailable[index].props.isSelected = !availableOptions[index].props.isSelected; + setAvailableOptions(newAvailable); + } + }; + + return ( + + + x.props.isSelected).length} of ${ + availableOptions.length + } options selected`} + > + } + /> + + + option.props.isSelected)} + onClick={() => moveSelected(true)} + aria-label="Add selected" + > + + + moveAll(true)} + aria-label="Add all" + > + + + moveAll(false)} + aria-label="Remove all" + > + + + moveSelected(false)} + isDisabled={!chosenOptions.some((option) => option.props.isSelected)} + aria-label="Remove selected" + > + + + + x.props.isSelected).length} of ${chosenOptions.length} options selected`} + isChosen + > + } + /> + + + + ); +}; diff --git a/packages/react-drag-drop/src/components/DragDrop/examples/DragDropDemos.md b/packages/react-drag-drop/src/components/DragDrop/examples/DragDropDemos.md index 9a23855c643..305f8061d66 100644 --- a/packages/react-drag-drop/src/components/DragDrop/examples/DragDropDemos.md +++ b/packages/react-drag-drop/src/components/DragDrop/examples/DragDropDemos.md @@ -14,7 +14,7 @@ import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-d import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon'; -import { DragDropSort } from '@patternfly/react-drag-drop'; +import { DragDropSort, DragDropContainer, Droppable as NewDroppable } from '@patternfly/react-drag-drop'; ## Sorting demos @@ -35,3 +35,27 @@ To enable reordering in a `` pane wrap the ``, place one or more `` components within the container, and define the `variant` property on all components. A collection of all draggable items should be passed to ``, and each `` should be passed their respective draggable items. + +`` will create the component's usual `children` internally based on the `items` property, so `children` should not be passed where the `` is defined. + +To avoid a wrapping div inserted by ``, pass the desired container element to the `wrapper` property. + +### Data list + +To enable multiple drop zones with `` components, place one or more `` within `` and define the `variant` on all components as "DataList". + +```ts file="./DragDropContainerDataList.tsx" + +``` + +### Dual list selector + +To enable multiple drop zones in a ``, wrap the `` component with ``, and then include a `` component within each pane. Both `` and `` should define the `variant` property as "DualListSelectorList". + +```ts file="./DragDropContainerDualListSelector.tsx" + +``` diff --git a/packages/react-drag-drop/src/components/DragDrop/index.ts b/packages/react-drag-drop/src/components/DragDrop/index.ts index ddc081716b1..837a360d018 100644 --- a/packages/react-drag-drop/src/components/DragDrop/index.ts +++ b/packages/react-drag-drop/src/components/DragDrop/index.ts @@ -1 +1,3 @@ export * from './DragDropSort'; +export * from './Droppable'; +export * from './DragDropContainer'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/DragButton.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DragButton.tsx deleted file mode 100644 index c28fee20d31..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/DragButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { css } from '@patternfly/react-styles'; -import dragButtonStyles from '@patternfly/react-styles/css/components/DataList/data-list'; -import buttonStyles from '@patternfly/react-styles/css/components/Button/button'; -import GripVerticalIcon from '@patternfly/react-icons/dist/esm/icons/grip-vertical-icon'; - -export interface DragButtonProps extends React.HTMLProps { - /** Additional classes added to the drag button */ - className?: string; - /** Sets button type */ - type?: 'button' | 'submit' | 'reset'; - /** Flag indicating if drag is disabled for the item */ - isDisabled?: boolean; -} - -export const DragButton: React.FunctionComponent = ({ className, ...props }: DragButtonProps) => ( - -); -DragButton.displayName = 'DragButton'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/DragDropSort.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DragDropSort.tsx deleted file mode 100644 index 615533711d8..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/DragDropSort.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import { css } from '@patternfly/react-styles'; -import { - DndContext, - closestCenter, - DragOverlay, - DndContextProps, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - DragEndEvent, - DragStartEvent -} from '@dnd-kit/core'; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - verticalListSortingStrategy -} from '@dnd-kit/sortable'; -import { Draggable } from './Draggable'; -import { DraggableDataListItem } from './DraggableDataListItem'; -import { DraggableDualListSelectorListItem } from './DraggableDualListSelectorListItem'; -import styles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; -import { canUseDOM } from '@patternfly/react-core'; - -export type DragDropSortDragEndEvent = DragEndEvent; -export type DragDropSortDragStartEvent = DragStartEvent; - -export interface DraggableObject { - /** Unique id of the draggable object */ - id: string; - /** Content rendered in the draggable object */ - content: React.ReactNode; - /** Props spread to the rendered wrapper of the draggable object */ - props?: any; -} - -/** - * DragDropSortProps extends dnd-kit's props which may be viewed at https://docs.dndkit.com/api-documentation/context-provider#props. - */ -export interface DragDropSortProps extends DndContextProps { - /** Custom defined content wrapper for draggable items. By default, draggable items are wrapped in a styled div. - * Intended to be a 'DataList' or 'DualListSelectorList' without children. */ - children?: React.ReactElement; - /** Sorted array of draggable objects */ - items: DraggableObject[]; - /** Callback when user drops a draggable object */ - onDrop: (event: DragDropSortDragEndEvent, items: DraggableObject[], oldIndex?: number, newIndex?: number) => void; - /** Callback when use begins dragging a draggable object */ - onDrag?: (event: DragDropSortDragStartEvent, oldIndex: number) => void; - /** The variant determines which component wraps the draggable object. - * Default and defaultWithHandle varaints wrap the draggable object in a div. - * DataList vairant wraps the draggable object in a DataListItem - * DualListSelectorList variant wraps the draggable objects in a DualListSelectorListItem and a div.pf-c-dual-list-selector__item-text element - * TableComposable variant wraps the draggable objects in TODO - * */ - variant?: 'default' | 'defaultWithHandle' | 'DataList' | 'DualListSelectorList' | 'TableComposable'; -} - -export const DragDropSort: React.FunctionComponent = ({ - items, - onDrop = () => {}, - onDrag = () => {}, - variant = 'default', - children, - ...props -}: DragDropSortProps) => { - const [activeId, setActiveId] = React.useState(null); - const itemIds = React.useMemo(() => (items ? Array.from(items, (item) => item.id as string) : []), [items]); - - const getItemById = (id: string): DraggableObject => items.find((item) => item.id === id); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates - }) - ); - - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - const oldIndex = itemIds.indexOf(active.id as string); - const newIndex = itemIds.indexOf(over.id as string); - const newItems = arrayMove(items, oldIndex, newIndex); - onDrop(event, newItems, oldIndex, newIndex); - setActiveId(null); - }; - - const handleDragStart = (event: DragStartEvent) => { - setActiveId(event.active.id as string); - onDrag(event, itemIds.indexOf(event.active.id as string)); - }; - - const getDragOverlay = () => { - if (!activeId) { - return; - } - const item = getItemById(activeId); - - let content; - switch (variant) { - case 'DualListSelectorList': - content = ( - - {item.content} - - ); - break; - case 'DataList': - content = ( - - {item.content} - - ); - break; - default: - content = ( - - {item.content} - - ); - } - - return ( -
- {content} -
- ); - }; - - const dragOverlay = {activeId && getDragOverlay()}; - - const renderedChildren = ( - - {items.map((item: DraggableObject) => { - switch (variant) { - case 'DualListSelectorList': - return ( - - {item.content} - - ); - case 'DataList': - return ( - - {item.content} - - ); - default: - return ( - - {item.content} - - ); - } - })} - {canUseDOM ? ReactDOM.createPortal(dragOverlay, document.getElementById('root')) : dragOverlay} - - ); - - return ( - - {children && - React.cloneElement(children, { - children: renderedChildren - })} - {!children &&
{renderedChildren}
} -
- ); -}; -DragDropSort.displayName = 'DragDropSort'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/Draggable.tsx b/packages/react-drag-drop/src/next/components/DragDrop/Draggable.tsx deleted file mode 100644 index c675f9236de..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/Draggable.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react'; -import { useSortable } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import { css } from '@patternfly/react-styles'; -import styles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; -import { DragButton } from './DragButton'; - -export interface DraggableProps extends React.HTMLProps { - /** Content rendered inside DragDrop */ - children?: React.ReactNode; - /** Class to add to outer div */ - className?: string; - /** @hide Id of the sortable context. */ - id?: string; - /** Flag indicating the draggable element should include a drag button. */ - useDragButton?: boolean; -} - -export const Draggable: React.FunctionComponent = ({ - children, - id, - className, - useDragButton = false, - ...props -}: DraggableProps) => { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id - }); - - const style = { - transform: CSS.Transform.toString(transform), - transition - }; - - return useDragButton ? ( -
- - {children} -
- ) : ( -
- {children} -
- ); -}; -Draggable.displayName = 'Draggable'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/DraggableDataListItem.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DraggableDataListItem.tsx deleted file mode 100644 index cbe83c945c2..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/DraggableDataListItem.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from 'react'; -import { useSortable } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import { css } from '@patternfly/react-styles'; -import styles from '@patternfly/react-styles/css/components/DataList/data-list'; -import dragStyles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; -import { DragButton } from './DragButton'; -import { DataListItemRow, DataListControl } from '@patternfly/react-core'; - -export interface DraggableDataListItemObject { - id?: string; - content?: React.ReactNode; -} - -export interface DraggableDataListItemProps extends React.HTMLProps { - /** Content rendered inside DragDrop */ - children?: React.ReactNode; - /** Class to add to outer div */ - className?: string; - /** @hide Id of the sortable context. */ - id?: string; -} - -export const DraggableDataListItem: React.FunctionComponent = ({ - children, - id, - className, - ...props -}: DraggableDataListItemProps) => { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id - }); - - const style = { - transform: CSS.Transform.toString(transform), - transition - }; - - return ( -
  • - - - - - {children} - -
  • - ); -}; -DraggableDataListItem.displayName = 'DraggableDataListItem'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx deleted file mode 100644 index 86415f89e7b..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import * as React from 'react'; -import { useSortable } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import { css } from '@patternfly/react-styles'; -import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; -import dragStyles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; -import { DragButton } from './DragButton'; -import { DualListSelectorListContext } from '@patternfly/react-core/dist/esm/deprecated/components/DualListSelector'; - -export interface DraggableDualListSelectorListItemProps extends React.HTMLProps { - /** Content rendered inside DragDrop */ - children?: React.ReactNode; - /** Don't wrap the component in a div. Requires passing a single child. */ - hasNoWrapper?: boolean; - /** Class to add to outer div */ - className?: string; - /** @hide Id of the sortable context */ - id?: string; - /** Flag indicating the list item is currently selected. */ - isSelected?: boolean; - /** Callback fired when an option is selected. */ - onOptionSelect?: (e: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, id?: string) => void; - /** @hide Internal field used to keep track of order of unfiltered options. */ - orderIndex?: number; - /** @hide Forwarded ref */ - innerRef?: React.RefObject; - /** Flag indicating if the dual list selector is in a disabled state */ - isDisabled?: boolean; -} - -export const DraggableDualListSelectorListItem: React.FunctionComponent = ({ - children, - id, - className, - orderIndex, - isSelected, - onOptionSelect, - ...props -}: DraggableDualListSelectorListItemProps) => { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id, - animateLayoutChanges: () => false - }); - - const { setFocusedOption } = React.useContext(DualListSelectorListContext); - - const style = { - transform: CSS.Transform.toString(transform), - transition - }; - - return ( - - ); -}; -DraggableDualListSelectorListItem.displayName = 'DraggableDualListSelectorListItem'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx b/packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx deleted file mode 100644 index ccb05fe9e26..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { useDroppable } from '@dnd-kit/core'; - -interface DroppableProps extends React.HTMLProps { - /** Content rendered inside DragDrop */ - children?: React.ReactNode; - /** Class to add to outer div */ - className?: string; - /** Name of zone that items can be dragged between. Should specify if there is more than one Droppable on the page. */ - zone?: string; - /** Id to be passed back on drop events */ - droppableId?: string; - /** Don't wrap the component in a div. Requires passing a single child. */ - hasNoWrapper?: boolean; -} - -export const Droppable: React.FunctionComponent = ({ children, ...props }: DroppableProps) => { - const { isOver, setNodeRef } = useDroppable({ id: 'droppable' }); - const style = { color: isOver ? 'green' : undefined }; - - return ( -
    - {children} -
    - ); -}; -Droppable.displayName = 'Droppable'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/DroppableContext.ts b/packages/react-drag-drop/src/next/components/DragDrop/DroppableContext.ts deleted file mode 100644 index 888cb4fc66f..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/DroppableContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from 'react'; - -export const DroppableContext = React.createContext({ - zone: 'defaultDroppableZone', - droppableId: 'defaultDroppableId' -}); diff --git a/packages/react-drag-drop/src/next/components/DragDrop/__tests__/DragDrop.test.tsx b/packages/react-drag-drop/src/next/components/DragDrop/__tests__/DragDrop.test.tsx deleted file mode 100644 index 98a08bf4c91..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/__tests__/DragDrop.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { DragDropSort } from '../'; - -jest.mock('react-dom', () => ({ - ...jest.requireActual('react-dom'), - createPortal: jest.fn((node) => node) -})); - -test('renders some divs', () => { - const { asFragment } = render( -
    - {}} - /> -
    - ); - expect(asFragment()).toMatchSnapshot(); -}); diff --git a/packages/react-drag-drop/src/next/components/DragDrop/__tests__/__snapshots__/DragDrop.test.tsx.snap b/packages/react-drag-drop/src/next/components/DragDrop/__tests__/__snapshots__/DragDrop.test.tsx.snap deleted file mode 100644 index a8127357ad8..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/__tests__/__snapshots__/DragDrop.test.tsx.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders some divs 1`] = ` - -
    -
    -
    - one -
    -
    - two -
    -
    - three -
    -
    - -
    -
    - -`; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSortingWithDragButton.tsx b/packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSortingWithDragButton.tsx deleted file mode 100644 index 0cea4a69962..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSortingWithDragButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { DragDropSort, DraggableObject } from '@patternfly/react-drag-drop'; - -export const BasicSortingWithDragButton: React.FunctionComponent = () => { - const [items, setItems] = React.useState([ - { id: 'with-button-1', content: 'one' }, - { id: 'with-button-2', content: 'two' }, - { id: 'with-button-3', content: 'three' } - ]); - - return ( - { - setItems(newItems); - }} - /> - ); -}; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/examples/DataListDraggable.tsx b/packages/react-drag-drop/src/next/components/DragDrop/examples/DataListDraggable.tsx deleted file mode 100644 index ce80773061e..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/examples/DataListDraggable.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { DataList, DataListCell, DataListCheck, DataListControl, DataListItemCells } from '@patternfly/react-core'; -import { DragDropSort, DraggableObject } from '@patternfly/react-drag-drop'; - -const getItems = (count: number): DraggableObject[] => - Array.from({ length: count }, (_, idx) => idx).map((idx) => ({ - id: `data-list-item-${idx}`, - content: ( - <> - - - - - {`item-${idx}`} - - ]} - /> - - ) - })); - -export const DataListDraggable: React.FunctionComponent = (props) => { - const [items, setItems] = React.useState(getItems(10)); - - return ( - { - setItems(newItems); - }} - variant="DataList" - > - - - ); -}; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDrop.md b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDrop.md deleted file mode 100644 index 3b0e3116f09..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDrop.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -id: Drag and drop -section: components -cssPrefix: pf-c-drag-drop -propComponents: ['DragDropSort', 'DraggableObject'] -hideNavItem: true -beta: true ---- - -Note: This drag and drop implementation lives in its own package at [@patternfly/react-drag-drop](https://www.npmjs.com/package/@patternfly/react-drag-drop)! - -import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon'; -import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon'; -import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon'; -import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; -import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon'; - -import { DragDropSort } from '@patternfly/react-drag-drop'; - -## Sorting examples - -### Basic drag and drop sorting - -```ts file="./BasicSorting.tsx" - -``` - -### Basic drag and drop sorting with drag button - -```ts file="./BasicSortingWithDragButton.tsx" - -``` diff --git a/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropDemos.md b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropDemos.md deleted file mode 100644 index 0be4b53e561..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropDemos.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: Drag and drop -section: components -source: react-next-demos -propComponents: ['DragDropSort', 'DraggableObject'] -beta: true ---- - -Note: This drag and drop implementation lives in its own package at [@patternfly/react-drag-drop](https://www.npmjs.com/package/@patternfly/react-drag-drop)! - -import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon'; -import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon'; -import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon'; -import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; -import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon'; - -import { DragDropSort } from '@patternfly/react-drag-drop'; - -## Sorting demos - -To enable drag and drop for compatible components, wrap the component with ``, define the `variant` property, and pass both the sortable `items` and `onDrop` callback to ``. `` will create the component's usual `children` internally based on the `items` property, so `children` should not be passed to the wrapped component. - -### Drag and drop sortable data list - -To enable reordering in a ``, wrap the `` component with `` and define the `variant` as "DataList". - -```ts file="./DataListDraggable.tsx" - -``` - -### Drag and drop sortable dual list selector - -To enable reordering in a `` pane wrap the `` component with `` and define the `variant` as "DualListSelectorList". - -```ts file="./DualListSelectorDraggable.tsx" - -``` diff --git a/packages/react-drag-drop/src/next/components/DragDrop/examples/DualListSelectorDraggable.tsx b/packages/react-drag-drop/src/next/components/DragDrop/examples/DualListSelectorDraggable.tsx deleted file mode 100644 index 21329fe1476..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/examples/DualListSelectorDraggable.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React from 'react'; -import { - DualListSelector, - DualListSelectorPane, - DualListSelectorList, - DualListSelectorListItem, - DualListSelectorControlsWrapper, - DualListSelectorControl -} from '@patternfly/react-core'; -import { DragDropSort, DraggableObject } from '@patternfly/react-drag-drop'; - -import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon'; -import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon'; -import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon'; -import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; - -export const ComposableDualListSelector: React.FunctionComponent = () => { - const [ignoreNextOptionSelect, setIgnoreNextOptionSelect] = React.useState(false); - const [availableOptions, setAvailableOptions] = React.useState([ - { id: 'Apple', content: 'Apple', props: { key: 'Apple', isSelected: false } }, - { id: 'Banana', content: 'Banana', props: { key: 'Banana', isSelected: false } }, - { id: 'Pineapple', content: 'Pineapple', props: { key: 'Pineapple', isSelected: false } } - ]); - - const [chosenOptions, setChosenOptions] = React.useState([ - { id: 'Orange', content: 'Orange', props: { key: 'Orange', isSelected: false } }, - { id: 'Grape', content: 'Grape', props: { key: 'Grape', isSelected: false } }, - { id: 'Peach', content: 'Peach', props: { key: 'Peach', isSelected: false } }, - { id: 'Strawberry', content: 'Strawberry', props: { key: 'Strawberry', isSelected: false } } - ]); - - const moveSelected = (fromAvailable) => { - const sourceOptions = fromAvailable ? availableOptions : chosenOptions; - const destinationOptions = fromAvailable ? chosenOptions : availableOptions; - for (let i = 0; i < sourceOptions.length; i++) { - const option = sourceOptions[i]; - if (option.props.isSelected) { - sourceOptions.splice(i, 1); - destinationOptions.push(option); - option.props.isSelected = false; - i--; - } - } - if (fromAvailable) { - setAvailableOptions([...sourceOptions]); - setChosenOptions([...destinationOptions]); - } else { - setChosenOptions([...sourceOptions]); - setAvailableOptions([...destinationOptions]); - } - }; - - const moveAll = (fromAvailable) => { - if (fromAvailable) { - setChosenOptions([...availableOptions, ...chosenOptions]); - setAvailableOptions([]); - } else { - setAvailableOptions([...chosenOptions, ...availableOptions]); - setChosenOptions([]); - } - }; - - const onOptionSelect = (event, index, isChosen) => { - if (ignoreNextOptionSelect) { - setIgnoreNextOptionSelect(false); - return; - } - if (isChosen) { - const newChosen = [...chosenOptions]; - newChosen[index].props.isSelected = !chosenOptions[index].props.isSelected; - setChosenOptions(newChosen); - } else { - const newAvailable = [...availableOptions]; - newAvailable[index].props.isSelected = !availableOptions[index].props.isSelected; - setAvailableOptions(newAvailable); - } - }; - - return ( - - x.props.isSelected).length} of ${ - availableOptions.length - } options selected`} - > - - {availableOptions.map((option, index) => ( - onOptionSelect(e, index, false)} - > - {option.content} - - ))} - - - - option.props.isSelected)} - onClick={() => moveSelected(true)} - aria-label="Add selected" - icon={} - /> - moveAll(true)} - aria-label="Add all" - icon={} - /> - moveAll(false)} - aria-label="Remove all" - icon={} - /> - moveSelected(false)} - isDisabled={!chosenOptions.some((option) => option.props.isSelected)} - aria-label="Remove selected" - icon={} - /> - - x.props.isSelected).length} of ${chosenOptions.length} options selected`} - isChosen - > - ({ - ...option, - props: { - key: option.props.key, - isSelected: option.props.isSelected, - onOptionSelect: (e) => onOptionSelect(e, index, true) - } - }))} - onDrop={(_, newItems) => { - setChosenOptions(newItems); - }} - variant="DualListSelectorList" - > - - - - - ); -}; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/index.ts b/packages/react-drag-drop/src/next/components/DragDrop/index.ts deleted file mode 100644 index ddc081716b1..00000000000 --- a/packages/react-drag-drop/src/next/components/DragDrop/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DragDropSort'; diff --git a/packages/react-drag-drop/src/next/components/index.ts b/packages/react-drag-drop/src/next/components/index.ts deleted file mode 100644 index 99bbd1480e6..00000000000 --- a/packages/react-drag-drop/src/next/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './DragDrop'; diff --git a/packages/react-drag-drop/src/next/index.ts b/packages/react-drag-drop/src/next/index.ts deleted file mode 100644 index 07635cbbc8e..00000000000 --- a/packages/react-drag-drop/src/next/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './components';