Skip to content

Commit 100d214

Browse files
authored
[EuiDraggable] Add support for reparenting dragged items (#8048)
1 parent b7b8f1d commit 100d214

21 files changed

+614
-131
lines changed
Loading
Loading
Loading
Loading
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- Updated `EuiDraggable` with a new `usePortal` prop.
2+
- This prop portals the dragged element to the body, allowing it to escape stacking contexts which prevents buggy drag positioning in e.g. popovers, modals, and flyouts.
3+
4+
**Deprecations**
5+
6+
- Deprecated `EuiPopover`'s `hasDragDrop` prop. Use `EuiDraggable`'s new `usePortal` prop instead.
7+

packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_example.js

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from 'react';
2-
import { Link } from 'react-router-dom';
32

43
import { GuideSectionTypes } from '../../components';
54
import {
@@ -34,12 +33,12 @@ const dragAndDropTypesSource = require('!!raw-loader!./drag_and_drop_types');
3433
import DragAndDropClone from './drag_and_drop_clone';
3534
const dragAndDropCloneSource = require('!!raw-loader!./drag_and_drop_clone');
3635

36+
import DragAndDropPortal from './drag_and_drop_portal';
37+
const dragAndDropPortalSource = require('!!raw-loader!./drag_and_drop_portal');
38+
3739
import DragAndDropComplex from './drag_and_drop_complex';
3840
const dragAndDropComplexSource = require('!!raw-loader!./drag_and_drop_complex');
3941

40-
import DragAndDropInPopover from './in_popover';
41-
const dragAndDropInPopoverSource = require('!!raw-loader!./in_popover');
42-
4342
export const DragAndDropExample = {
4443
title: 'Drag and drop',
4544
intro: (
@@ -343,31 +342,11 @@ export const DragAndDropExample = {
343342
demo: <DragAndDropClone />,
344343
},
345344
{
346-
title: 'Kitchen sink',
345+
title: 'Portalled items',
347346
source: [
348347
{
349348
type: GuideSectionTypes.JS,
350-
code: dragAndDropComplexSource,
351-
},
352-
],
353-
text: (
354-
<>
355-
<p>
356-
<strong>EuiDraggables</strong> in <strong>EuiDroppables</strong>,{' '}
357-
<strong>EuiDroppables</strong> in <strong>EuiDraggables</strong>,
358-
custom drag handles, horizontal movement, vertical movement,
359-
flexbox, panel inception, you name it.
360-
</p>
361-
</>
362-
),
363-
demo: <DragAndDropComplex />,
364-
},
365-
{
366-
title: 'Using drag and drop in popovers',
367-
source: [
368-
{
369-
type: GuideSectionTypes.TSX,
370-
code: dragAndDropInPopoverSource,
349+
code: dragAndDropPortalSource,
371350
},
372351
],
373352
text: (
@@ -385,18 +364,67 @@ export const DragAndDropExample = {
385364
.
386365
</p>
387366
<p>
388-
This behavior particularly affects{' '}
389-
<Link to="/layout/popover">
390-
<strong>EuiPopover</strong>
391-
</Link>
392-
. If using drag and drop UX within a popover, you{' '}
393-
<strong>must</strong> include the{' '}
394-
<EuiCode>{'<EuiPopover hasDragDrop>'}</EuiCode> prop for items to
395-
properly render while being dragged.
367+
To ensure dragging works as expected inside e.g.{' '}
368+
<strong>EuiFlyout</strong>, <strong>EuiModal</strong> or{' '}
369+
<strong>EuiPopover</strong> use the prop{' '}
370+
<EuiCode>usePortal</EuiCode> on <strong>EuiDraggable</strong>{' '}
371+
components. This will render the currently dragged element inside a
372+
portal appended to the document body (or wherever{' '}
373+
<strong>EuiPortal</strong> is configured to{' '}
374+
<EuiCode>insert</EuiCode> to by default).
396375
</p>
376+
<EuiCallOut color="warning" title="Style inheritance">
377+
<p>
378+
If the styling of the your draggable content is scoped to a parent
379+
component, the styling won't be applied while dragging it when
380+
using <EuiCode>usePortal</EuiCode>. This is due to the portalled
381+
position in the DOM, which changes previous hierarchical relations
382+
to other ancestor elements. To prevent this from happening, we
383+
recommend applying styling from within the{' '}
384+
<strong>EuiDraggable</strong> scope without any parent selectors.
385+
</p>
386+
</EuiCallOut>
397387
</>
398388
),
399-
demo: <DragAndDropInPopover />,
389+
snippet: `<EuiDragDropContext onDragEnd={onDragEnd}>
390+
<EuiDroppable droppableId="DROPPABLE_AREA">
391+
<EuiDraggable
392+
spacing="m"
393+
key="DRAGGABLE_ID"
394+
index={0}
395+
draggableId="DRAGGABLE_ID"
396+
usePortal
397+
>
398+
{(provided, state) => (
399+
<EuiPanel hasShadow={state.isDragging}>
400+
Item 1
401+
{state.isDragging && ' ✨'}
402+
</EuiPanel>
403+
)}
404+
</EuiDraggable>
405+
</EuiDroppable>
406+
</EuiDragDropContext>`,
407+
demo: <DragAndDropPortal />,
408+
},
409+
{
410+
title: 'Kitchen sink',
411+
source: [
412+
{
413+
type: GuideSectionTypes.JS,
414+
code: dragAndDropComplexSource,
415+
},
416+
],
417+
text: (
418+
<>
419+
<p>
420+
<strong>EuiDraggables</strong> in <strong>EuiDroppables</strong>,{' '}
421+
<strong>EuiDroppables</strong> in <strong>EuiDraggables</strong>,
422+
custom drag handles, horizontal movement, vertical movement,
423+
flexbox, panel inception, you name it.
424+
</p>
425+
</>
426+
),
427+
demo: <DragAndDropComplex />,
400428
},
401429
],
402430
};
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import React, { FunctionComponent, ReactElement, useState } from 'react';
2+
import {
3+
EuiButton,
4+
EuiDragDropContext,
5+
EuiDraggable,
6+
EuiDroppable,
7+
EuiFlyout,
8+
EuiFlyoutBody,
9+
EuiFlyoutHeader,
10+
EuiModal,
11+
EuiModalBody,
12+
EuiModalHeader,
13+
EuiPanel,
14+
EuiPopover,
15+
EuiSpacer,
16+
EuiTitle,
17+
euiDragDropReorder,
18+
} from '../../../../src/components';
19+
import { htmlIdGenerator } from '../../../../src/services';
20+
import { DroppableProps, OnDragEndResponder } from '@hello-pangea/dnd';
21+
22+
const makeId = htmlIdGenerator();
23+
24+
const makeList = (number: number, start = 1) =>
25+
Array.from({ length: number }, (v, k) => k + start).map((el) => {
26+
return {
27+
content: `Item ${el}`,
28+
id: makeId(),
29+
};
30+
});
31+
32+
const DragContainer: FunctionComponent<{
33+
children: ReactElement | ReactElement[] | DroppableProps['children'];
34+
onDragEnd: OnDragEndResponder;
35+
}> = ({ children, onDragEnd }) => (
36+
<EuiDragDropContext onDragEnd={onDragEnd}>
37+
<EuiDroppable droppableId="DROPPABLE_AREA" spacing="m">
38+
{children}
39+
</EuiDroppable>
40+
</EuiDragDropContext>
41+
);
42+
43+
export default () => {
44+
const [isFlyoutOpen, setFlyoutOpen] = useState(false);
45+
const [isModalOpen, setModalOpen] = useState(false);
46+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
47+
48+
const [list, setList] = useState(makeList(3));
49+
const onDragEnd: OnDragEndResponder = ({ source, destination }) => {
50+
if (source && destination) {
51+
const items = euiDragDropReorder(list, source.index, destination.index);
52+
53+
setList(items);
54+
}
55+
};
56+
57+
return (
58+
<>
59+
<EuiButton onClick={() => setFlyoutOpen(!isFlyoutOpen)}>
60+
Toggle flyout
61+
</EuiButton>
62+
<EuiSpacer />
63+
<EuiButton onClick={() => setModalOpen(!isModalOpen)}>
64+
Toggle modal
65+
</EuiButton>
66+
67+
{isFlyoutOpen && (
68+
<EuiFlyout onClose={() => setFlyoutOpen(false)}>
69+
<EuiFlyoutHeader>
70+
<EuiTitle size="s">
71+
<h2>
72+
Portalled <strong>EuiDraggable</strong> items
73+
</h2>
74+
</EuiTitle>
75+
</EuiFlyoutHeader>
76+
<EuiFlyoutBody>
77+
<DragContainer onDragEnd={onDragEnd}>
78+
{list.map(({ content, id }, idx) => (
79+
<EuiDraggable
80+
spacing="m"
81+
key={id}
82+
index={idx}
83+
draggableId={id}
84+
usePortal
85+
>
86+
{(provided, state) => (
87+
<EuiPanel hasShadow={state.isDragging}>
88+
{content}
89+
{state.isDragging && ' ✨'}
90+
</EuiPanel>
91+
)}
92+
</EuiDraggable>
93+
))}
94+
</DragContainer>
95+
</EuiFlyoutBody>
96+
</EuiFlyout>
97+
)}
98+
99+
{isModalOpen && (
100+
<EuiModal onClose={() => setModalOpen(false)}>
101+
<EuiModalHeader>
102+
<EuiTitle size="s">
103+
<h2>
104+
Portalled <strong>EuiDraggable</strong> items
105+
</h2>
106+
</EuiTitle>
107+
</EuiModalHeader>
108+
<EuiModalBody>
109+
<DragContainer onDragEnd={onDragEnd}>
110+
{list.map(({ content, id }, idx) => (
111+
<EuiDraggable
112+
spacing="m"
113+
key={id}
114+
index={idx}
115+
draggableId={id}
116+
usePortal
117+
>
118+
{(provided, state) => (
119+
<EuiPanel hasShadow={state.isDragging}>
120+
{content}
121+
{state.isDragging && ' ✨'}
122+
</EuiPanel>
123+
)}
124+
</EuiDraggable>
125+
))}
126+
</DragContainer>
127+
</EuiModalBody>
128+
</EuiModal>
129+
)}
130+
131+
<EuiSpacer />
132+
133+
<EuiPopover
134+
isOpen={isPopoverOpen}
135+
closePopover={() => setIsPopoverOpen(false)}
136+
button={
137+
<EuiButton onClick={() => setIsPopoverOpen(!isPopoverOpen)}>
138+
Toggle popover
139+
</EuiButton>
140+
}
141+
panelPaddingSize="none"
142+
panelProps={{ css: { inlineSize: 200 } }}
143+
>
144+
<DragContainer
145+
onDragEnd={({ source, destination }) => {
146+
if (source && destination) {
147+
const items = euiDragDropReorder(
148+
list,
149+
source.index,
150+
destination.index
151+
);
152+
setList(items);
153+
}
154+
}}
155+
>
156+
{list.map(({ content, id }, idx) => (
157+
<EuiDraggable
158+
spacing="m"
159+
key={id}
160+
index={idx}
161+
draggableId={id}
162+
usePortal
163+
>
164+
{(provided, state) => (
165+
<EuiPanel hasShadow={state.isDragging}>{content}</EuiPanel>
166+
)}
167+
</EuiDraggable>
168+
))}
169+
</DragContainer>
170+
</EuiPopover>
171+
</>
172+
);
173+
};

packages/eui/src-docs/src/views/drag_and_drop/in_popover.tsx

Lines changed: 0 additions & 63 deletions
This file was deleted.

packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ exports[`useDataGridColumnSelector columnSelector renders a toolbar button/popov
5050
aria-describedby="generated-id"
5151
aria-live="off"
5252
aria-modal="true"
53-
class="euiPanel euiPanel--plain euiPopover__panel emotion-euiPanel-grow-m-plain-euiPopover__panel-light-hasDragDrop-bottom"
53+
class="euiPanel euiPanel--plain euiPopover__panel emotion-euiPanel-grow-m-plain-euiPopover__panel-light-hasTransform"
5454
data-autofocus="true"
5555
data-popover-panel="true"
5656
role="dialog"

packages/eui/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ exports[`DataGridSortingControl renders a toolbar button/popover allowing users
5050
aria-describedby="generated-id"
5151
aria-live="off"
5252
aria-modal="true"
53-
class="euiPanel euiPanel--plain euiPanel--paddingSmall euiPopover__panel emotion-euiPanel-grow-m-s-plain-euiPopover__panel-light-hasDragDrop-bottom"
53+
class="euiPanel euiPanel--plain euiPanel--paddingSmall euiPopover__panel emotion-euiPanel-grow-m-s-plain-euiPopover__panel-light-hasTransform"
5454
data-autofocus="true"
5555
data-popover-panel="true"
5656
role="dialog"

0 commit comments

Comments
 (0)