diff --git a/packages/react-core/src/components/DataList/examples/DataList.md b/packages/react-core/src/components/DataList/examples/DataList.md index 88e96dd7b4f..722f5846ddc 100644 --- a/packages/react-core/src/components/DataList/examples/DataList.md +++ b/packages/react-core/src/components/DataList/examples/DataList.md @@ -86,9 +86,9 @@ import global_BorderWidth_sm from '@patternfly/react-tokens/dist/esm/global_Bord ### Draggable -Draggable data lists used to have their own HTML5-based API for drag and drop, which wasn't able to fulfill requirements such as custom styling on items being dragged. So we wrote generic `DragDrop`, `Draggable`, and `Droppable` components for this purpose. Use those new components instead of the deprecated (and buggy!) HTML5-based API. +Note: There is a new recommended drag and drop implementation with full keyboard functionality, which replaces this implementation. To adhere to our new recommendations, refer to the [drag and drop demos](/components/drag-and-drop/react-next-demos). -Note: Keyboard accessibility and screen reader accessibility for the `DragDrop` component are still in development. +Previously, draggable data lists had their own API for the [drag and drop component](/components/drag-and-drop), which wasn't flexible enough to allow custom styling for items as they are dragged. To address this disparity, ``, ``, and `` components were added to replace our now deprecated HTML5-based API. Keyboard and screen reader accessibility for the `` component is still in development. ```ts isBeta file="./DataListDraggable.tsx" diff --git a/packages/react-core/src/components/DragDrop/examples/DragDrop.md b/packages/react-core/src/components/DragDrop/examples/DragDrop.md index b3a379223ee..fee8fc41dae 100644 --- a/packages/react-core/src/components/DragDrop/examples/DragDrop.md +++ b/packages/react-core/src/components/DragDrop/examples/DragDrop.md @@ -2,10 +2,10 @@ id: Drag and drop section: components propComponents: [DragDrop, Draggable, Droppable, DraggableItemPosition] -beta: true +title: Drag and drop --- -You can use the `DragDrop` component to move items in or between lists. The `DragDrop` component should contain `Droppable` components which contain `Draggable` components. +You can use the `` component to move items in or between lists. The `` component should contain `` components which contain `` components. ```ts noLive import React from 'react'; @@ -35,9 +35,11 @@ Note: Keyboard accessibility and screen reader accessibility are still in develo ### Basic ```ts file="./DragDropBasic.tsx" + ``` ### Multiple lists ```ts file="./DragDropMultipleLists.tsx" + ``` diff --git a/packages/react-core/src/components/DragDrop/examples/DragDropBasic.tsx b/packages/react-core/src/components/DragDrop/examples/DragDropBasic.tsx index 684c315bcf5..d80a6a7f213 100644 --- a/packages/react-core/src/components/DragDrop/examples/DragDropBasic.tsx +++ b/packages/react-core/src/components/DragDrop/examples/DragDropBasic.tsx @@ -20,14 +20,14 @@ const getItems = (count: number) => })); const reorder = (list: ItemType[], startIndex: number, endIndex: number) => { - const result = list; + const result = [...list]; const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); return result; }; export const DragDropBasic: React.FunctionComponent = () => { - const [items, setItems] = React.useState(getItems(10)); + const [items, setItems] = React.useState(getItems(10)); function onDrop(source: SourceType, dest: DestinationType) { if (dest) { @@ -42,8 +42,8 @@ export const DragDropBasic: React.FunctionComponent = () => { return ( - {items.map(({ content }, i) => ( - + {items.map(({ id, content }) => ( + {content} ))} diff --git a/packages/react-core/src/components/DragDrop/examples/DragDropMultipleLists.tsx b/packages/react-core/src/components/DragDrop/examples/DragDropMultipleLists.tsx index 50cc54658c8..0400633987f 100644 --- a/packages/react-core/src/components/DragDrop/examples/DragDropMultipleLists.tsx +++ b/packages/react-core/src/components/DragDrop/examples/DragDropMultipleLists.tsx @@ -11,6 +11,11 @@ interface SourceType { index: number; } +interface MultipleListState { + items1: ItemType[]; + items2: ItemType[]; +} + interface DestinationType extends SourceType {} const getItems = (count: number, startIndex: number) => @@ -35,7 +40,7 @@ const move = (source: ItemType[], destination: ItemType[], sourceIndex: number, }; export const DragDropMultipleLists: React.FunctionComponent = () => { - const [items, setItems] = React.useState({ + const [items, setItems] = React.useState({ items1: getItems(10, 0), items2: getItems(5, 10) }); @@ -84,7 +89,7 @@ export const DragDropMultipleLists: React.FunctionComponent = () => { {Object.entries(items).map(([key, subitems]) => ( - {subitems.map(({ id, content }) => ( + {(subitems as ItemType[]).map(({ id, content }) => ( {content} diff --git a/packages/react-core/src/components/DragDrop/index.ts b/packages/react-core/src/components/DragDrop/index.ts index 032ae9dfe38..7a4a6bd342e 100644 --- a/packages/react-core/src/components/DragDrop/index.ts +++ b/packages/react-core/src/components/DragDrop/index.ts @@ -1,3 +1,4 @@ export * from './DragDrop'; export * from './Draggable'; export * from './Droppable'; +export * from './DroppableContext'; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md b/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md index 60288c158ff..8ee90f265a4 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md @@ -87,6 +87,8 @@ The dual list selector can also be built in a composable manner to make customiz ### Composable with drag and drop +Note: There is a new recommended drag and drop implementation with full keyboard functionality, which replaces this implementation. To adhere to our new recommendations, refer to the [drag and drop demos](/components/drag-and-drop/react-next-demos). + This example only allows reordering the contents of the "chosen" pane with drag and drop. To make a pane able to be reordered: - wrap the `DualListSelectorPane` in a `DragDrop` component @@ -99,7 +101,7 @@ This example only allows reordering the contents of the "chosen" pane with drag - define an `onDrag` callback which ensures that the drag event will not cross hairs with the `onOptionSelect` click event set on the option. Note: the `ignoreNextOptionSelect` state value is used to prevent selection while dragging. -Note: Keyboard accessibility and screen reader accessibility for the `DragDrop` component are still in development. +Keyboard and screen reader accessibility for the `` component is still in development. ```ts file="DualListSelectorComposableDragDrop.tsx" diff --git a/packages/react-core/src/components/DualListSelector/index.ts b/packages/react-core/src/components/DualListSelector/index.ts index d2b6deb96ff..7f66aa3a037 100644 --- a/packages/react-core/src/components/DualListSelector/index.ts +++ b/packages/react-core/src/components/DualListSelector/index.ts @@ -1,7 +1,9 @@ export * from './DualListSelector'; +export * from './DualListSelectorContext'; export * from './DualListSelectorControl'; export * from './DualListSelectorControlsWrapper'; export * from './DualListSelectorPane'; export * from './DualListSelectorList'; export * from './DualListSelectorListItem'; export * from './DualListSelectorTree'; +export * from './DualListSelectorContext'; diff --git a/packages/react-core/src/components/index.ts b/packages/react-core/src/components/index.ts index a322279493d..a4338497ba5 100644 --- a/packages/react-core/src/components/index.ts +++ b/packages/react-core/src/components/index.ts @@ -22,6 +22,7 @@ export * from './DataList'; export * from './DatePicker'; export * from './DescriptionList'; export * from './Divider'; +export * from './DragDrop'; export * from './Drawer'; export * from './Dropdown'; export * from './DualListSelector'; @@ -76,7 +77,6 @@ export * from './Tooltip'; export * from './NumberInput'; export * from './TreeView'; export * from './Wizard'; -export * from './DragDrop'; export * from './TextInputGroup'; export * from './Panel'; export * from './Truncate'; diff --git a/packages/react-docs/package.json b/packages/react-docs/package.json index fc1e451822e..f08f1908e8e 100644 --- a/packages/react-docs/package.json +++ b/packages/react-docs/package.json @@ -29,6 +29,7 @@ "@patternfly/react-icons": "^5.2.0-prerelease.9", "@patternfly/react-styles": "^5.2.0-prerelease.6", "@patternfly/react-table": "^5.2.0-prerelease.41", + "@patternfly/react-drag-drop": "^5.2.0-prelease.0", "@patternfly/react-tokens": "^5.2.0-prerelease.7" }, "devDependencies": { diff --git a/packages/react-docs/patternfly-docs/patternfly-docs.source.js b/packages/react-docs/patternfly-docs/patternfly-docs.source.js index b90f892c8d3..0d6f20116d6 100644 --- a/packages/react-docs/patternfly-docs/patternfly-docs.source.js +++ b/packages/react-docs/patternfly-docs/patternfly-docs.source.js @@ -16,12 +16,14 @@ module.exports = (baseSourceMD, sourceProps) => { const reactCodeEditorPath = require .resolve('@patternfly/react-code-editor/package.json') .replace('package.json', 'src'); + const reactDragDropPath = require.resolve('@patternfly/react-drag-drop/package.json').replace('package.json', 'src'); const reactPropsIgnore = '**/*.test.tsx'; sourceProps(path.join(reactCorePath, '/**/*.tsx'), reactPropsIgnore); sourceProps(path.join(reactTablePath, '/**/*.tsx'), reactPropsIgnore); sourceProps(path.join(reactChartsPath, '/**/*.tsx'), reactPropsIgnore); sourceProps(path.join(reactCodeEditorPath, '/**/*.tsx'), reactPropsIgnore); + sourceProps(path.join(reactDragDropPath, '/**/*.tsx'), reactPropsIgnore); // React MD sourceMD(path.join(reactCorePath, '/components/**/examples/*.md'), 'react'); @@ -41,6 +43,9 @@ module.exports = (baseSourceMD, sourceProps) => { // Code Editor MD sourceMD(path.join(reactCodeEditorPath, '/**/examples/*.md'), 'react'); + // Drag drop MD + sourceMD(path.join(reactDragDropPath, '/**/examples/*.md'), 'react-next'); + // OUIA MD sourceMD(path.join(reactCorePath, 'helpers/OUIA/OUIA.md'), 'react'); }; diff --git a/packages/react-drag-drop/package.json b/packages/react-drag-drop/package.json new file mode 100644 index 00000000000..5c78ee321c7 --- /dev/null +++ b/packages/react-drag-drop/package.json @@ -0,0 +1,49 @@ +{ + "name": "@patternfly/react-drag-drop", + "version": "5.2.0-prelease.0", + "description": "PatternFly drag and drop solution", + "main": "dist/js/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "patternfly:src": "src/", + "repository": { + "type": "git", + "url": "https://github.com/patternfly/patternfly-react.git" + }, + "keywords": [ + "react", + "patternfly", + "drag-drop" + ], + "author": "Red Hat", + "license": "MIT", + "bugs": { + "url": "https://github.com/patternfly/patternfly-react/issues" + }, + "homepage": "https://github.com/patternfly/patternfly-react/tree/main/packages/react-drag-drop#readme", + "scripts": { + "clean": "rimraf dist" + }, + "dependencies": { + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", + "@dnd-kit/sortable": "^7.0.2", + "@patternfly/react-core": "^5.2.0-prerelease.41", + "@patternfly/react-icons": "^5.2.0-prerelease.9", + "@patternfly/react-styles": "^5.2.0-prerelease.6", + "memoize-one": "^5.1.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + }, + "devDependencies": { + "rimraf": "^2.6.2", + "typescript": "^4.7.4" + } +} diff --git a/packages/react-drag-drop/src/index.ts b/packages/react-drag-drop/src/index.ts new file mode 100644 index 00000000000..4007789b7ae --- /dev/null +++ b/packages/react-drag-drop/src/index.ts @@ -0,0 +1 @@ +export * from './next'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/DragButton.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DragButton.tsx new file mode 100644 index 00000000000..c28fee20d31 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/DragButton.tsx @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000000..35e9892d171 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/DragDropSort.tsx @@ -0,0 +1,174 @@ +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 { DraggableObject } from './DragDropUtil'; +import styles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; + +export type DragDropSortDragEndEvent = DragEndEvent; +export type DragDropSortDragStartEvent = DragStartEvent; + +/** + * 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 renderedChildren = ( + + {items.map((item: DraggableObject) => { + switch (variant) { + case 'DualListSelectorList': + return ( + + {item.content} + + ); + case 'DataList': + return ( + + {item.content} + + ); + default: + return ( + + {item.content} + + ); + } + })} + {activeId && getDragOverlay()} + + ); + + return ( + + {children && + React.cloneElement(children, { + children: renderedChildren + })} + {!children &&
{renderedChildren}
} +
+ ); +}; +DragDropSort.displayName = 'DragDropSort'; diff --git a/packages/react-drag-drop/src/next/components/DragDrop/DragDropUtil.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DragDropUtil.tsx new file mode 100644 index 00000000000..3dc0faa7de2 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/DragDropUtil.tsx @@ -0,0 +1,8 @@ +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; +} diff --git a/packages/react-drag-drop/src/next/components/DragDrop/Draggable.tsx b/packages/react-drag-drop/src/next/components/DragDrop/Draggable.tsx new file mode 100644 index 00000000000..c675f9236de --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/Draggable.tsx @@ -0,0 +1,58 @@ +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 new file mode 100644 index 00000000000..cbe83c945c2 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/DraggableDataListItem.tsx @@ -0,0 +1,61 @@ +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 new file mode 100644 index 00000000000..7e92d6c481b --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx @@ -0,0 +1,93 @@ +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/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 new file mode 100644 index 00000000000..ccb05fe9e26 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/Droppable.tsx @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000000..888cb4fc66f --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/DroppableContext.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000000..e8831b934a4 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/__tests__/DragDrop.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { DragDropSort } from '../'; + +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 new file mode 100644 index 00000000000..a8127357ad8 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/__tests__/__snapshots__/DragDrop.test.tsx.snap @@ -0,0 +1,59 @@ +// 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/BasicSorting.tsx b/packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSorting.tsx new file mode 100644 index 00000000000..40021ef9f60 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSorting.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { DragDropSort, DraggableObject } from '@patternfly/react-drag-drop'; + +export const BasicSorting: React.FunctionComponent = () => { + const [items, setItems] = React.useState([ + { id: 'basic-1', content: 'one' }, + { id: 'basic-2', content: 'two' }, + { id: 'basic-3', content: 'three' } + ]); + + return ( + { + setItems(newItems); + }} + /> + ); +}; 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 new file mode 100644 index 00000000000..0cea4a69962 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/examples/BasicSortingWithDragButton.tsx @@ -0,0 +1,20 @@ +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 new file mode 100644 index 00000000000..ce80773061e --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/examples/DataListDraggable.tsx @@ -0,0 +1,38 @@ +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 new file mode 100644 index 00000000000..195ddaf26c4 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDrop.md @@ -0,0 +1,32 @@ +--- +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, DraggableObject } 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 new file mode 100644 index 00000000000..3cc75caf79c --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/examples/DragDropDemos.md @@ -0,0 +1,37 @@ +--- +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, DraggableObject } 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 new file mode 100644 index 00000000000..ffee53c87e9 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/examples/DualListSelectorDraggable.tsx @@ -0,0 +1,154 @@ +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" + > + + + 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 + > + ({ + ...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 new file mode 100644 index 00000000000..c8fbb82358b --- /dev/null +++ b/packages/react-drag-drop/src/next/components/DragDrop/index.ts @@ -0,0 +1,2 @@ +export * from './DragDropSort'; +export * from './DragDropUtil'; diff --git a/packages/react-drag-drop/src/next/components/index.ts b/packages/react-drag-drop/src/next/components/index.ts new file mode 100644 index 00000000000..99bbd1480e6 --- /dev/null +++ b/packages/react-drag-drop/src/next/components/index.ts @@ -0,0 +1 @@ +export * from './DragDrop'; diff --git a/packages/react-drag-drop/src/next/index.ts b/packages/react-drag-drop/src/next/index.ts new file mode 100644 index 00000000000..07635cbbc8e --- /dev/null +++ b/packages/react-drag-drop/src/next/index.ts @@ -0,0 +1 @@ +export * from './components'; diff --git a/packages/react-drag-drop/tsconfig.cjs.json b/packages/react-drag-drop/tsconfig.cjs.json new file mode 100644 index 00000000000..578d46af9d3 --- /dev/null +++ b/packages/react-drag-drop/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/js", + "module": "commonjs", + "tsBuildInfoFile": "dist/cjs.tsbuildinfo" + } +} diff --git a/packages/react-drag-drop/tsconfig.json b/packages/react-drag-drop/tsconfig.json new file mode 100644 index 00000000000..f08aeb83272 --- /dev/null +++ b/packages/react-drag-drop/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/esm", + "tsBuildInfoFile": "dist/esm.tsbuildinfo" + }, + "include": [ + "./src/*", + "./src/**/*" + ], + "references": [ + { + "path": "../react-core" + }, + { + "path": "../react-icons" + }, + { + "path": "../react-styles" + } + ] +} diff --git a/packages/tsconfig.cjs.json b/packages/tsconfig.cjs.json index 6a79555e886..607146bbcac 100644 --- a/packages/tsconfig.cjs.json +++ b/packages/tsconfig.cjs.json @@ -10,6 +10,9 @@ { "path": "./react-core/tsconfig.cjs.json" }, + { + "path": "./react-drag-drop/tsconfig.cjs.json" + }, { "path": "./react-icons/tsconfig.cjs.json" }, diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 8e6c82bdb5f..b7ea8c38508 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -10,6 +10,9 @@ { "path": "./react-core" }, + { + "path": "./react-drag-drop" + }, { "path": "./react-icons" }, diff --git a/scripts/promote.sh b/scripts/promote.sh index 0464729f77b..26118a1e43d 100755 --- a/scripts/promote.sh +++ b/scripts/promote.sh @@ -4,6 +4,7 @@ packages=( @patternfly/react-charts @patternfly/react-code-editor @patternfly/react-core + @patternfly/react-drag-drop @patternfly/react-icons @patternfly/react-styles @patternfly/react-table diff --git a/yarn.lock b/yarn.lock index 6f803d242d2..dfb3f388665 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2932,6 +2932,45 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@dnd-kit/accessibility@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz#3ccbefdfca595b0a23a5dc57d3de96bc6935641c" + integrity sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.0.8": + version "6.0.8" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.0.8.tgz#040ae13fea9787ee078e5f0361f3b49b07f3f005" + integrity sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA== + dependencies: + "@dnd-kit/accessibility" "^3.0.0" + "@dnd-kit/utilities" "^3.2.1" + tslib "^2.0.0" + +"@dnd-kit/modifiers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz#9e39b25fd6e323659604cc74488fe044d33188c8" + integrity sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A== + dependencies: + "@dnd-kit/utilities" "^3.2.1" + tslib "^2.0.0" + +"@dnd-kit/sortable@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-7.0.2.tgz#791d550872457f3f3c843e00d159b640f982011c" + integrity sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA== + dependencies: + "@dnd-kit/utilities" "^3.2.0" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.0", "@dnd-kit/utilities@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.1.tgz#53f9e2016fd2506ec49e404c289392cfff30332a" + integrity sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA== + dependencies: + tslib "^2.0.0" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -13973,6 +14012,11 @@ memfs@^3.1.2, memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" +memoize-one@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz" @@ -17047,6 +17091,11 @@ requires-port@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -19075,6 +19124,11 @@ tslib@^1.9.0: version "1.13.0" resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz" +tslib@^2.0.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tslib@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz"