Skip to content

[EuiDraggable] Add support for reparenting dragged items #8048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions packages/eui/changelogs/upcoming/8048.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- Updated `EuiDraggable` with a new `usePortal` prop.
- 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.

**Deprecations**

- Deprecated `EuiPopover`'s `hasDragDrop` prop. Use `EuiDraggable`'s new `usePortal` prop instead.

Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { Link } from 'react-router-dom';

import { GuideSectionTypes } from '../../components';
import {
Expand Down Expand Up @@ -34,12 +33,12 @@ const dragAndDropTypesSource = require('!!raw-loader!./drag_and_drop_types');
import DragAndDropClone from './drag_and_drop_clone';
const dragAndDropCloneSource = require('!!raw-loader!./drag_and_drop_clone');

import DragAndDropPortal from './drag_and_drop_portal';
const dragAndDropPortalSource = require('!!raw-loader!./drag_and_drop_portal');

import DragAndDropComplex from './drag_and_drop_complex';
const dragAndDropComplexSource = require('!!raw-loader!./drag_and_drop_complex');

import DragAndDropInPopover from './in_popover';
const dragAndDropInPopoverSource = require('!!raw-loader!./in_popover');

export const DragAndDropExample = {
title: 'Drag and drop',
intro: (
Expand Down Expand Up @@ -343,31 +342,11 @@ export const DragAndDropExample = {
demo: <DragAndDropClone />,
},
{
title: 'Kitchen sink',
title: 'Portalled items',
source: [
{
type: GuideSectionTypes.JS,
code: dragAndDropComplexSource,
},
],
text: (
<>
<p>
<strong>EuiDraggables</strong> in <strong>EuiDroppables</strong>,{' '}
<strong>EuiDroppables</strong> in <strong>EuiDraggables</strong>,
custom drag handles, horizontal movement, vertical movement,
flexbox, panel inception, you name it.
</p>
</>
),
demo: <DragAndDropComplex />,
},
{
title: 'Using drag and drop in popovers',
source: [
{
type: GuideSectionTypes.TSX,
code: dragAndDropInPopoverSource,
code: dragAndDropPortalSource,
},
],
text: (
Expand All @@ -385,18 +364,67 @@ export const DragAndDropExample = {
.
</p>
<p>
This behavior particularly affects{' '}
<Link to="/layout/popover">
<strong>EuiPopover</strong>
</Link>
. If using drag and drop UX within a popover, you{' '}
<strong>must</strong> include the{' '}
<EuiCode>{'<EuiPopover hasDragDrop>'}</EuiCode> prop for items to
properly render while being dragged.
To ensure dragging works as expected inside e.g.{' '}
<strong>EuiFlyout</strong>, <strong>EuiModal</strong> or{' '}
<strong>EuiPopover</strong> use the prop{' '}
<EuiCode>usePortal</EuiCode> on <strong>EuiDraggable</strong>{' '}
components. This will render the currently dragged element inside a
portal appended to the document body (or wherever{' '}
<strong>EuiPortal</strong> is configured to{' '}
<EuiCode>insert</EuiCode> to by default).
</p>
<EuiCallOut color="warning" title="Style inheritance">
<p>
If the styling of the your draggable content is scoped to a parent
component, the styling won't be applied while dragging it when
using <EuiCode>usePortal</EuiCode>. This is due to the portalled
position in the DOM, which changes previous hierarchical relations
to other ancestor elements. To prevent this from happening, we
recommend applying styling from within the{' '}
<strong>EuiDraggable</strong> scope without any parent selectors.
</p>
</EuiCallOut>
</>
),
demo: <DragAndDropInPopover />,
snippet: `<EuiDragDropContext onDragEnd={onDragEnd}>
<EuiDroppable droppableId="DROPPABLE_AREA">
<EuiDraggable
spacing="m"
key="DRAGGABLE_ID"
index={0}
draggableId="DRAGGABLE_ID"
usePortal
>
{(provided, state) => (
<EuiPanel hasShadow={state.isDragging}>
Item 1
{state.isDragging && ' ✨'}
</EuiPanel>
)}
</EuiDraggable>
</EuiDroppable>
</EuiDragDropContext>`,
demo: <DragAndDropPortal />,
},
{
title: 'Kitchen sink',
source: [
{
type: GuideSectionTypes.JS,
code: dragAndDropComplexSource,
},
],
text: (
<>
<p>
<strong>EuiDraggables</strong> in <strong>EuiDroppables</strong>,{' '}
<strong>EuiDroppables</strong> in <strong>EuiDraggables</strong>,
custom drag handles, horizontal movement, vertical movement,
flexbox, panel inception, you name it.
</p>
</>
),
demo: <DragAndDropComplex />,
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React, { FunctionComponent, ReactElement, useState } from 'react';
import {
EuiButton,
EuiDragDropContext,
EuiDraggable,
EuiDroppable,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiPanel,
EuiPopover,
EuiSpacer,
EuiTitle,
euiDragDropReorder,
} from '../../../../src/components';
import { htmlIdGenerator } from '../../../../src/services';
import { DroppableProps, OnDragEndResponder } from '@hello-pangea/dnd';

const makeId = htmlIdGenerator();

const makeList = (number: number, start = 1) =>
Array.from({ length: number }, (v, k) => k + start).map((el) => {
return {
content: `Item ${el}`,
id: makeId(),
};
});

const DragContainer: FunctionComponent<{
children: ReactElement | ReactElement[] | DroppableProps['children'];
onDragEnd: OnDragEndResponder;
}> = ({ children, onDragEnd }) => (
<EuiDragDropContext onDragEnd={onDragEnd}>
<EuiDroppable droppableId="DROPPABLE_AREA" spacing="m">
{children}
</EuiDroppable>
</EuiDragDropContext>
);

export default () => {
const [isFlyoutOpen, setFlyoutOpen] = useState(false);
const [isModalOpen, setModalOpen] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);

const [list, setList] = useState(makeList(3));
const onDragEnd: OnDragEndResponder = ({ source, destination }) => {
if (source && destination) {
const items = euiDragDropReorder(list, source.index, destination.index);

setList(items);
}
};

return (
<>
<EuiButton onClick={() => setFlyoutOpen(!isFlyoutOpen)}>
Toggle flyout
</EuiButton>
<EuiSpacer />
<EuiButton onClick={() => setModalOpen(!isModalOpen)}>
Toggle modal
</EuiButton>

{isFlyoutOpen && (
<EuiFlyout onClose={() => setFlyoutOpen(false)}>
<EuiFlyoutHeader>
<EuiTitle size="s">
<h2>
Portalled <strong>EuiDraggable</strong> items
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<DragContainer onDragEnd={onDragEnd}>
{list.map(({ content, id }, idx) => (
<EuiDraggable
spacing="m"
key={id}
index={idx}
draggableId={id}
usePortal
>
{(provided, state) => (
<EuiPanel hasShadow={state.isDragging}>
{content}
{state.isDragging && ' ✨'}
</EuiPanel>
)}
</EuiDraggable>
))}
</DragContainer>
</EuiFlyoutBody>
</EuiFlyout>
)}

{isModalOpen && (
<EuiModal onClose={() => setModalOpen(false)}>
<EuiModalHeader>
<EuiTitle size="s">
<h2>
Portalled <strong>EuiDraggable</strong> items
</h2>
</EuiTitle>
</EuiModalHeader>
<EuiModalBody>
<DragContainer onDragEnd={onDragEnd}>
{list.map(({ content, id }, idx) => (
<EuiDraggable
spacing="m"
key={id}
index={idx}
draggableId={id}
usePortal
>
{(provided, state) => (
<EuiPanel hasShadow={state.isDragging}>
{content}
{state.isDragging && ' ✨'}
</EuiPanel>
)}
</EuiDraggable>
))}
</DragContainer>
</EuiModalBody>
</EuiModal>
)}

<EuiSpacer />

<EuiPopover
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
button={
<EuiButton onClick={() => setIsPopoverOpen(!isPopoverOpen)}>
Toggle popover
</EuiButton>
}
panelPaddingSize="none"
panelProps={{ css: { inlineSize: 200 } }}
>
<DragContainer
onDragEnd={({ source, destination }) => {
if (source && destination) {
const items = euiDragDropReorder(
list,
source.index,
destination.index
);
setList(items);
}
}}
>
{list.map(({ content, id }, idx) => (
<EuiDraggable
spacing="m"
key={id}
index={idx}
draggableId={id}
usePortal
>
{(provided, state) => (
<EuiPanel hasShadow={state.isDragging}>{content}</EuiPanel>
)}
</EuiDraggable>
))}
</DragContainer>
</EuiPopover>
</>
);
};
63 changes: 0 additions & 63 deletions packages/eui/src-docs/src/views/drag_and_drop/in_popover.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ exports[`useDataGridColumnSelector columnSelector renders a toolbar button/popov
aria-describedby="generated-id"
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--plain euiPopover__panel emotion-euiPanel-grow-m-plain-euiPopover__panel-light-hasDragDrop-bottom"
class="euiPanel euiPanel--plain euiPopover__panel emotion-euiPanel-grow-m-plain-euiPopover__panel-light-hasTransform"
data-autofocus="true"
data-popover-panel="true"
role="dialog"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ exports[`DataGridSortingControl renders a toolbar button/popover allowing users
aria-describedby="generated-id"
aria-live="off"
aria-modal="true"
class="euiPanel euiPanel--plain euiPanel--paddingSmall euiPopover__panel emotion-euiPanel-grow-m-s-plain-euiPopover__panel-light-hasDragDrop-bottom"
class="euiPanel euiPanel--plain euiPanel--paddingSmall euiPopover__panel emotion-euiPanel-grow-m-s-plain-euiPopover__panel-light-hasTransform"
data-autofocus="true"
data-popover-panel="true"
role="dialog"
Expand Down
Loading
Loading