Skip to content

Commit a0bd26f

Browse files
committed
refactor(draggable-container, draggable-item): re-build using the useDraggable hook
Rebuilds the draggable component using the new useDraggable hook. During this work the type of the `id` prop on `DraggableItem` has been limited to string only to align with HTML types as the `id` prop is now passed to the DOM. BREAKING CHANGE: The `id` prop on `DraggableItem` now only accepts a string type
1 parent 982d7aa commit a0bd26f

File tree

11 files changed

+254
-294
lines changed

11 files changed

+254
-294
lines changed

src/components/draggable/__internal__/drop-target.component.tsx

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 51 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import React, { useEffect, useState, useRef, useMemo } from "react";
2-
import { DndProvider } from "react-dnd";
3-
import { HTML5Backend } from "react-dnd-html5-backend";
1+
import React, { useMemo, forwardRef } from "react";
42
import { MarginProps } from "styled-system";
5-
63
import invariant from "invariant";
74
import { filterStyledSystemMarginProps } from "../../style/utils";
85
import DraggableItem from "./draggable-item/draggable-item.component";
9-
import DropTarget from "./__internal__/drop-target.component";
6+
import { StyledDraggableContainer } from "./draggable-item/draggable-item.style";
107
import { TagProps } from "../../__internal__/utils/helpers/tags";
8+
import useDraggable, { UseDraggableHandle } from "../../hooks/useDraggable";
119

1210
export interface DraggableContainerProps extends MarginProps, TagProps {
1311
/** Callback fired when order is changed */
@@ -28,108 +26,56 @@ export interface DraggableContainerProps extends MarginProps, TagProps {
2826
flexDirection?: "row" | "row-reverse";
2927
}
3028

31-
const DraggableContainer = ({
32-
"data-element": dataElement,
33-
"data-role": dataRole = "draggable-container",
34-
children,
35-
getOrder,
36-
flexDirection = "row",
37-
...rest
38-
}: DraggableContainerProps): JSX.Element => {
39-
const [draggableItems, setDraggableItems] = useState(
40-
React.Children.toArray(children),
41-
);
42-
43-
const hasProperChildren = useMemo(() => {
44-
const invalidChild = React.Children.toArray(children).find(
45-
(child) =>
46-
!React.isValidElement(child) ||
47-
(child.type as React.FunctionComponent).displayName !== "DraggableItem",
48-
);
49-
return !invalidChild;
50-
}, [children]);
51-
52-
// `<DraggableItem />` is required to make `Draggable` work
53-
invariant(
54-
hasProperChildren,
55-
`\`${DraggableContainer.displayName}\` only accepts children of type \`${DraggableItem.displayName}\`.`,
56-
);
57-
58-
const isFirstRender = useRef(true);
59-
60-
useEffect(() => {
61-
if (!isFirstRender.current) {
62-
setDraggableItems(React.Children.toArray(children));
63-
} else {
64-
isFirstRender.current = false;
65-
}
66-
}, [children]);
67-
68-
const findItem = (id: string | number) => {
69-
const draggableItem = draggableItems.filter((item) => {
70-
return React.isValidElement(item) && `${item.props.id}` === id;
71-
})[0];
72-
73-
return {
74-
draggableItem,
75-
index: draggableItems.indexOf(draggableItem),
76-
};
77-
};
78-
79-
const moveItem = (id: string, atIndex: number) => {
80-
const { draggableItem, index } = findItem(id);
81-
82-
// istanbul ignore if
83-
if (!draggableItem) return;
84-
85-
const copyOfDraggableItems = [...draggableItems];
86-
copyOfDraggableItems.splice(index, 1);
87-
copyOfDraggableItems.splice(atIndex, 0, draggableItem);
88-
setDraggableItems(copyOfDraggableItems);
89-
};
90-
91-
const getItemsId = (item?: string | number) => {
92-
if (!getOrder) {
93-
return;
94-
}
95-
96-
const draggableItemIds = draggableItems.map(
97-
(draggableItem) => (draggableItem as { props: { id: string } }).props.id,
29+
const DraggableContainer = forwardRef<
30+
UseDraggableHandle,
31+
DraggableContainerProps
32+
>(
33+
(
34+
{
35+
"data-element": dataElement,
36+
"data-role": dataRole = "draggable-container",
37+
children,
38+
getOrder,
39+
flexDirection = "row",
40+
...rest
41+
}: DraggableContainerProps,
42+
ref,
43+
): JSX.Element => {
44+
const hasProperChildren = useMemo(() => {
45+
const invalidChild = React.Children.toArray(children).find(
46+
(child) =>
47+
!React.isValidElement(child) ||
48+
(child.type as React.FunctionComponent).displayName !==
49+
"DraggableItem",
50+
);
51+
return !invalidChild;
52+
}, [children]);
53+
54+
// `<DraggableItem />` is required to make `Draggable` work
55+
invariant(
56+
hasProperChildren,
57+
`\`${DraggableContainer.displayName}\` only accepts children of type \`${DraggableItem.displayName}\`.`,
9858
);
9959

100-
getOrder(draggableItemIds, item);
101-
};
102-
103-
const marginProps = filterStyledSystemMarginProps(rest);
104-
105-
return (
106-
<DndProvider backend={HTML5Backend}>
107-
<DropTarget
108-
data-element={dataElement}
109-
data-role={dataRole}
110-
getOrder={getItemsId}
111-
{...marginProps}
112-
>
113-
{draggableItems.map((item) => {
114-
return (
115-
React.isValidElement(item) &&
116-
React.cloneElement(
117-
item as React.ReactElement,
118-
{
119-
id: `${item.props.id}`,
120-
findItem,
121-
moveItem,
122-
flexDirection,
123-
},
124-
[item.props.children],
125-
)
126-
);
127-
})}
128-
</DropTarget>
129-
</DndProvider>
130-
);
131-
};
60+
const marginProps = filterStyledSystemMarginProps(rest);
61+
const items = Array.isArray(children) ? children : [children];
62+
63+
const { draggableElement } = useDraggable({
64+
draggableItems: items,
65+
containerNode: StyledDraggableContainer,
66+
getOrder,
67+
ref,
68+
containerProps: {
69+
"data-element": dataElement,
70+
"data-role": dataRole,
71+
flexDirection,
72+
...marginProps,
73+
},
74+
});
75+
76+
return draggableElement;
77+
},
78+
);
13279

13380
DraggableContainer.displayName = "DraggableContainer";
134-
13581
export default DraggableContainer;

src/components/draggable/draggable-item/draggable-item.component.tsx

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from "react";
2-
import { useDrop, useDrag } from "react-dnd";
32
import { PaddingProps } from "styled-system";
43
import tagComponent, {
54
TagProps,
@@ -15,91 +14,26 @@ export interface DraggableItemProps extends PaddingProps, TagProps {
1514
*
1615
* Use this prop to make `Draggable` work
1716
*/
18-
id: number | string;
17+
id: string;
1918
/** The content of the component. */
2019
children: React.ReactNode;
21-
/**
22-
* @private
23-
* @ignore
24-
*/
25-
findItem?: (id: string | number) => {
26-
DraggableItemProps: React.ReactElement;
27-
index: number;
28-
};
29-
/**
30-
* @private
31-
* @ignore
32-
*/
33-
moveItem?: (
34-
droppedId: string | number,
35-
overIndex: number | undefined,
36-
) => void;
37-
/**
38-
* @private
39-
* @ignore
40-
*/
41-
flexDirection?: "row" | "row-reverse";
4220
}
4321

4422
const DraggableItem = ({
4523
id,
46-
findItem,
47-
moveItem,
4824
children,
4925
py = 1,
50-
flexDirection,
5126
"data-element": dataElement,
5227
"data-role": dataRole = "draggable-item",
5328
...rest
5429
}: DraggableItemProps): JSX.Element => {
55-
let originalIndex;
56-
// istanbul ignore else
57-
if (findItem) {
58-
originalIndex = findItem(id)?.index;
59-
}
60-
const [{ isDragging }, drag] = useDrag({
61-
type: "draggableItem",
62-
item: { id, originalIndex },
63-
collect: (monitor) => ({
64-
isDragging: monitor.isDragging(),
65-
}),
66-
end: (dropResult, monitor) => {
67-
const { id: droppedId, originalIndex: oIndex } = monitor.getItem();
68-
const didDrop = monitor.didDrop();
69-
if (!didDrop && moveItem) {
70-
moveItem(droppedId, oIndex);
71-
}
72-
},
73-
});
74-
75-
interface DragItem {
76-
index: number;
77-
id: string;
78-
}
79-
80-
const [, drop] = useDrop({
81-
accept: "draggableItem",
82-
canDrop: () => false,
83-
hover(item: DragItem) {
84-
if (item?.id !== id && findItem) {
85-
const { index: overIndex } = findItem(id);
86-
// istanbul ignore else
87-
if (moveItem) {
88-
moveItem(item?.id, overIndex);
89-
}
90-
}
91-
},
92-
});
93-
9430
const paddingProps = filterStyledSystemPaddingProps(rest);
9531

9632
return (
9733
<StyledDraggableItem
98-
isDragging={isDragging}
99-
ref={(node) => drag(drop(node))}
10034
py={py}
101-
flexDirection={flexDirection}
10235
{...paddingProps}
36+
id={id}
10337
{...tagComponent("draggable-item", {
10438
"data-element": dataElement,
10539
"data-role": dataRole,

src/components/draggable/draggable-item/draggable-item.style.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import { padding, margin, PaddingProps } from "styled-system";
33

44
import applyBaseTheme from "../../../style/themes/apply-base-theme";
55

6-
const StyledDraggableContainer = styled.div.attrs(applyBaseTheme)`
7-
${margin}
8-
`;
9-
106
interface StyledDraggableItemProps extends PaddingProps {
7+
id: string;
118
isDragging?: boolean;
129
flexDirection?: "row" | "row-reverse";
1310
}
@@ -22,7 +19,25 @@ const StyledDraggableItem = styled.div.attrs(
2219
cursor: move;
2320
justify-content: space-between;
2421
flex-direction: ${({ flexDirection }) => flexDirection};
25-
opacity: ${({ isDragging }) => (isDragging ? "0" : "1")};
22+
`;
23+
24+
const StyledDraggableContainer = styled.div.attrs(applyBaseTheme)<{
25+
flexDirection: "row" | "row-reverse";
26+
}>`
27+
${margin}
28+
29+
${StyledDraggableItem} {
30+
flex-direction: ${({ flexDirection }) => flexDirection};
31+
transition: opacity 0.2s ease-in-out;
32+
}
33+
34+
[data-drag-state="is-being-dragged-over"] {
35+
opacity: 0;
36+
}
37+
38+
[data-drag-state="is-dragging"] {
39+
opacity: 0;
40+
}
2641
`;
2742

2843
export { StyledDraggableContainer, StyledDraggableItem };

0 commit comments

Comments
 (0)