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/next/components/DragDrop/DragDropContainer.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DragDropContainer.tsx new file mode 100644 index 00000000000..cffe8e81a24 --- /dev/null +++ b/packages/react-drag-drop/src/next/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 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'; + /** 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/next/components/DragDrop/DragDropSort.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DragDropSort.tsx index 615533711d8..b2c653b93a3 100644 --- a/packages/react-drag-drop/src/next/components/DragDrop/DragDropSort.tsx +++ b/packages/react-drag-drop/src/next/components/DragDrop/DragDropSort.tsx @@ -1,42 +1,11 @@ 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'; +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. */ @@ -57,6 +26,8 @@ export interface DragDropSortProps extends DndContextProps { * TableComposable variant wraps the draggable objects in TODO * */ variant?: 'default' | 'defaultWithHandle' | 'DataList' | 'DualListSelectorList' | 'TableComposable'; + /** Additional classes to apply to the drag overlay */ + overlayProps?: any; } export const DragDropSort: React.FunctionComponent = ({ @@ -65,122 +36,35 @@ 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 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} - - ); + const dropZoneId = props.id ? props.id : 'droppable'; + const containerVariant = variant === 'defaultWithHandle' ? 'default' : variant; return ( - - {children && - React.cloneElement(children, { - children: renderedChildren - })} - {!children &&
{renderedChildren}
} -
+ + ); }; DragDropSort.displayName = 'DragDropSort'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx b/packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx index ccb05fe9e26..2b3d650b20d 100644 --- a/packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx +++ b/packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx @@ -1,27 +1,81 @@ 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' | 'defaultWithHandle' | '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/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/examples/DataListDraggable.tsx b/packages/react-drag-drop/src/next/components/DragDrop/examples/DataListDraggable.tsx index ce80773061e..d1b4bf7d6a8 100644 --- a/packages/react-drag-drop/src/next/components/DragDrop/examples/DataListDraggable.tsx +++ b/packages/react-drag-drop/src/next/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/next/components/DragDrop/examples/DragDrop.md b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDrop.md index 3b0e3116f09..f61cadef780 100644 --- a/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDrop.md +++ b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDrop.md @@ -15,7 +15,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 examples @@ -30,3 +30,9 @@ import { DragDropSort } from '@patternfly/react-drag-drop'; ```ts file="./BasicSortingWithDragButton.tsx" ``` + +### Multiple drop zones + +```ts file="./DragDropContainerBasic.tsx" + +``` diff --git a/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropContainerBasic.tsx b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropContainerBasic.tsx new file mode 100644 index 00000000000..cd0e98f95f4 --- /dev/null +++ b/packages/react-drag-drop/src/next/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/next/components/DragDrop/examples/DragDropContainerDataList.tsx b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropContainerDataList.tsx new file mode 100644 index 00000000000..cd9a0cc7836 --- /dev/null +++ b/packages/react-drag-drop/src/next/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/next/components/DragDrop/examples/DragDropContainerDualListSelector.tsx b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropContainerDualListSelector.tsx new file mode 100644 index 00000000000..ea058fdb452 --- /dev/null +++ b/packages/react-drag-drop/src/next/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/next/components/DragDrop/examples/DragDropDemos.md b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropDemos.md index 0be4b53e561..617a796c70d 100644 --- a/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropDemos.md +++ b/packages/react-drag-drop/src/next/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/next/components/DragDrop/index.ts b/packages/react-drag-drop/src/next/components/DragDrop/index.ts index ddc081716b1..837a360d018 100644 --- a/packages/react-drag-drop/src/next/components/DragDrop/index.ts +++ b/packages/react-drag-drop/src/next/components/DragDrop/index.ts @@ -1 +1,3 @@ export * from './DragDropSort'; +export * from './Droppable'; +export * from './DragDropContainer';