Skip to content

Commit

Permalink
[EuiDraggable] Add support for reparenting dragged items (#8048)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgadewoll authored Oct 1, 2024
1 parent b7b8f1d commit 100d214
Show file tree
Hide file tree
Showing 21 changed files with 614 additions and 131 deletions.
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 />,
},
],
};
173 changes: 173 additions & 0 deletions packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_portal.tsx
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

0 comments on commit 100d214

Please sign in to comment.