Skip to content

Commit 362f976

Browse files
committed
feat(@clayui/demos): Drag & Drop Example
1 parent b0887de commit 362f976

File tree

5 files changed

+343
-61
lines changed

5 files changed

+343
-61
lines changed

packages/clay-list/src/Item.tsx

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -34,42 +34,49 @@ interface IProps extends React.HTMLAttributes<HTMLLIElement> {
3434
header?: boolean;
3535
}
3636

37-
const ClayListItem: React.FunctionComponent<IProps> = ({
38-
action = false,
39-
active = false,
40-
children,
41-
className,
42-
disabled = false,
43-
flex = false,
44-
header = false,
45-
...otherProps
46-
}: IProps) => {
47-
const [focus, setFocus] = React.useState(false);
37+
const ClayListItem = React.forwardRef<HTMLLIElement, IProps>(
38+
(
39+
{
40+
action = false,
41+
active = false,
42+
children,
43+
className,
44+
disabled = false,
45+
flex = false,
46+
header = false,
47+
...otherProps
48+
},
49+
ref
50+
) => {
51+
const [focus, setFocus] = React.useState(false);
52+
53+
return (
54+
<li
55+
{...otherProps}
56+
className={classNames(className, {
57+
active,
58+
focus,
59+
'list-group-header': header,
60+
'list-group-item': !header,
61+
'list-group-item-action': action && !disabled,
62+
'list-group-item-disabled': disabled,
63+
'list-group-item-flex': flex,
64+
})}
65+
onBlur={({currentTarget, relatedTarget}) => {
66+
if (
67+
relatedTarget &&
68+
!currentTarget.contains(relatedTarget as Node)
69+
) {
70+
setFocus(false);
71+
}
72+
}}
73+
onFocus={() => setFocus(true)}
74+
ref={ref}
75+
>
76+
{children}
77+
</li>
78+
);
79+
}
80+
);
4881

49-
return (
50-
<li
51-
{...otherProps}
52-
className={classNames(className, {
53-
active,
54-
focus,
55-
'list-group-header': header,
56-
'list-group-item': !header,
57-
'list-group-item-action': action && !disabled,
58-
'list-group-item-disabled': disabled,
59-
'list-group-item-flex': flex,
60-
})}
61-
onBlur={({currentTarget, relatedTarget}) => {
62-
if (
63-
relatedTarget &&
64-
!currentTarget.contains(relatedTarget as Node)
65-
) {
66-
setFocus(false);
67-
}
68-
}}
69-
onFocus={() => setFocus(true)}
70-
>
71-
{children}
72-
</li>
73-
);
74-
};
7582
export default ClayListItem;

packages/demos/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"@clayui/pagination": "*",
1717
"@clayui/pagination-bar": "*",
1818
"@clayui/panel": "*",
19-
"classnames": "^2.2.6"
20-
}
19+
"classnames": "^2.2.6",
20+
"react-dnd": "^10.0.2",
21+
"react-dnd-html5-backend": "^10.0.2"
22+
},
23+
"version": "0.0.1"
2124
}

packages/demos/stories/DragDrop.tsx

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* © 2020 Liferay, Inc. <https://liferay.com>
3+
*
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
*/
6+
7+
import ClayList from '@clayui/list';
8+
import {XYCoord} from 'dnd-core';
9+
import React, {useState, useCallback, useRef} from 'react';
10+
import {useDrag, useDrop, DropTargetMonitor} from 'react-dnd';
11+
12+
interface IDraggableListItemProps {
13+
/**
14+
* List Item unique identifier
15+
*/
16+
id: any;
17+
18+
/**
19+
* The text displayed in the List Item
20+
*/
21+
text: string;
22+
23+
/**
24+
* Index of the List Item in order to keep track of it's positioning within the array
25+
*/
26+
index: number;
27+
28+
/**
29+
* Function that handles the moving of List Items
30+
*/
31+
onMove: (dragIndex: number, hoverIndex: number) => void;
32+
}
33+
34+
interface IDragItem {
35+
/**
36+
* List Item unique identifier
37+
*/
38+
id: string;
39+
40+
/**
41+
* Index of the List Item in order to keep track of it's positioning within the array
42+
*/
43+
index: number;
44+
45+
/**
46+
* Type of data that is allowed to be dragged
47+
*/
48+
type: string;
49+
}
50+
51+
const DraggableListItem: React.FunctionComponent<IDraggableListItemProps> = ({
52+
id,
53+
index,
54+
onMove,
55+
text,
56+
}) => {
57+
const ref = useRef<HTMLLIElement>(null);
58+
59+
const [, drop] = useDrop({
60+
accept: 'listItem',
61+
hover(item: IDragItem, monitor: DropTargetMonitor) {
62+
const dragIndex = item.index;
63+
64+
const hoverIndex = index;
65+
66+
if (!ref.current || dragIndex === hoverIndex) {
67+
return;
68+
}
69+
70+
const hoverBoundingRect = ref.current!.getBoundingClientRect();
71+
72+
const verticalMiddle =
73+
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
74+
75+
const mousePosition = monitor.getClientOffset();
76+
77+
const pixelsToTop =
78+
(mousePosition as XYCoord).y - hoverBoundingRect.top;
79+
80+
const draggingUpwards =
81+
dragIndex > hoverIndex && pixelsToTop > verticalMiddle * 1.5;
82+
83+
const draggingDownwards =
84+
dragIndex < hoverIndex && pixelsToTop < verticalMiddle / 2;
85+
86+
if (draggingDownwards || draggingUpwards) {
87+
return;
88+
}
89+
90+
onMove(dragIndex, hoverIndex);
91+
92+
item.index = hoverIndex;
93+
},
94+
});
95+
96+
const [{isDragging, itemBeingDragged}, drag] = useDrag({
97+
collect: (monitor: any) => ({
98+
isDragging: monitor.isDragging(),
99+
itemBeingDragged: monitor.getItem() || {id: 0},
100+
}),
101+
item: {id, index, type: 'listItem'},
102+
});
103+
104+
const isItemBeingDragged = itemBeingDragged.id === id;
105+
106+
drag(drop(ref));
107+
108+
const style = {
109+
backgroundColor: isItemBeingDragged ? '#EFEFEF' : '',
110+
border: isItemBeingDragged ? '2px solid #555555' : '',
111+
cursor: 'grab',
112+
opacity: isDragging ? 0 : 1,
113+
};
114+
115+
return (
116+
<ClayList.Item
117+
className="m-2"
118+
flex
119+
id={`listItem${id}`}
120+
key={id}
121+
ref={ref}
122+
style={style}
123+
>
124+
<ClayList.ItemField>{`Item ${id}`}</ClayList.ItemField>
125+
126+
<ClayList.ItemField expand>
127+
<ClayList.ItemTitle>{`Item ${id} Title`}</ClayList.ItemTitle>
128+
<ClayList.ItemText>{text}</ClayList.ItemText>
129+
</ClayList.ItemField>
130+
</ClayList.Item>
131+
);
132+
};
133+
134+
const DraggableList: React.FunctionComponent = () => {
135+
const [listItems, setListItems] = useState([
136+
{
137+
id: 1,
138+
text: 'Write a cool JS library',
139+
},
140+
{
141+
id: 2,
142+
text: 'Make it generic enough',
143+
},
144+
{
145+
id: 3,
146+
text: 'Write README',
147+
},
148+
{
149+
id: 4,
150+
text: 'Create some examples',
151+
},
152+
{
153+
id: 5,
154+
text:
155+
'Spam in Twitter and IRC to promote it (note that this element is taller than the others)',
156+
},
157+
{
158+
id: 6,
159+
text: '???',
160+
},
161+
{
162+
id: 7,
163+
text: 'PROFIT',
164+
},
165+
]);
166+
167+
const onMove = useCallback(
168+
(dragIndex: number, hoverIndex: number) => {
169+
const tempList = [...listItems];
170+
171+
tempList.splice(dragIndex, 1);
172+
173+
tempList.splice(hoverIndex, 0, listItems[dragIndex]);
174+
175+
setListItems(tempList);
176+
},
177+
[listItems]
178+
);
179+
180+
return (
181+
<ClayList className="col-4 m-2">
182+
{listItems.map((listItem, index) => (
183+
<DraggableListItem
184+
id={listItem.id}
185+
index={index}
186+
key={listItem.id}
187+
onMove={onMove}
188+
text={listItem.text}
189+
/>
190+
))}
191+
</ClayList>
192+
);
193+
};
194+
195+
export default DraggableList;

packages/demos/stories/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@
77
import '@clayui/css/lib/css/atlas.css';
88
import {storiesOf} from '@storybook/react';
99
import React from 'react';
10+
import {DndProvider} from 'react-dnd';
11+
import Backend from 'react-dnd-html5-backend';
1012

13+
import DragDrop from './DragDrop';
1114
import FilesPage from './FilesPage';
1215
import FormPage from './FormPage';
1316
import ListPage from './ListPage';
1417

1518
storiesOf('Demos|Templates', module)
1619
.add('List Page', () => <ListPage />)
1720
.add('Files Page', () => <FilesPage />)
18-
.add('Form Page', () => <FormPage />);
21+
.add('Form Page', () => <FormPage />)
22+
.add('Drag & Drop', () => (
23+
<DndProvider backend={Backend}>
24+
<DragDrop />
25+
</DndProvider>
26+
));

0 commit comments

Comments
 (0)