diff --git a/packages/eui/.loki/reference/chrome_desktop_Display_EuiDragDropContext_Within_Flyouts.png b/packages/eui/.loki/reference/chrome_desktop_Display_EuiDragDropContext_Within_Flyouts.png new file mode 100644 index 00000000000..3a19fa07633 Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Display_EuiDragDropContext_Within_Flyouts.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Display_EuiDragDropContext_Within_Modals.png b/packages/eui/.loki/reference/chrome_desktop_Display_EuiDragDropContext_Within_Modals.png new file mode 100644 index 00000000000..2af48209dc9 Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Display_EuiDragDropContext_Within_Modals.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Display_EuiDragDropContext_Within_Flyouts.png b/packages/eui/.loki/reference/chrome_mobile_Display_EuiDragDropContext_Within_Flyouts.png new file mode 100644 index 00000000000..5e4cd5a805c Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Display_EuiDragDropContext_Within_Flyouts.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Display_EuiDragDropContext_Within_Modals.png b/packages/eui/.loki/reference/chrome_mobile_Display_EuiDragDropContext_Within_Modals.png new file mode 100644 index 00000000000..7bc192cf8f0 Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Display_EuiDragDropContext_Within_Modals.png differ diff --git a/packages/eui/changelogs/upcoming/8048.md b/packages/eui/changelogs/upcoming/8048.md new file mode 100644 index 00000000000..aca59ca4556 --- /dev/null +++ b/packages/eui/changelogs/upcoming/8048.md @@ -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. + diff --git a/packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_example.js b/packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_example.js index de190eafb4a..471f589e10f 100644 --- a/packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_example.js +++ b/packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_example.js @@ -1,5 +1,4 @@ import React from 'react'; -import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../components'; import { @@ -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: ( @@ -343,31 +342,11 @@ export const DragAndDropExample = { demo: , }, { - title: 'Kitchen sink', + title: 'Portalled items', source: [ { type: GuideSectionTypes.JS, - code: dragAndDropComplexSource, - }, - ], - text: ( - <> -

- EuiDraggables in EuiDroppables,{' '} - EuiDroppables in EuiDraggables, - custom drag handles, horizontal movement, vertical movement, - flexbox, panel inception, you name it. -

- - ), - demo: , - }, - { - title: 'Using drag and drop in popovers', - source: [ - { - type: GuideSectionTypes.TSX, - code: dragAndDropInPopoverSource, + code: dragAndDropPortalSource, }, ], text: ( @@ -385,18 +364,67 @@ export const DragAndDropExample = { .

- This behavior particularly affects{' '} - - EuiPopover - - . If using drag and drop UX within a popover, you{' '} - must include the{' '} - {''} prop for items to - properly render while being dragged. + To ensure dragging works as expected inside e.g.{' '} + EuiFlyout, EuiModal or{' '} + EuiPopover use the prop{' '} + usePortal on EuiDraggable{' '} + components. This will render the currently dragged element inside a + portal appended to the document body (or wherever{' '} + EuiPortal is configured to{' '} + insert to by default).

+ +

+ 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 usePortal. 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{' '} + EuiDraggable scope without any parent selectors. +

+
), - demo: , + snippet: ` + + + {(provided, state) => ( + + Item 1 + {state.isDragging && ' ✨'} + + )} + + + `, + demo: , + }, + { + title: 'Kitchen sink', + source: [ + { + type: GuideSectionTypes.JS, + code: dragAndDropComplexSource, + }, + ], + text: ( + <> +

+ EuiDraggables in EuiDroppables,{' '} + EuiDroppables in EuiDraggables, + custom drag handles, horizontal movement, vertical movement, + flexbox, panel inception, you name it. +

+ + ), + demo: , }, ], }; diff --git a/packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_portal.tsx b/packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_portal.tsx new file mode 100644 index 00000000000..eae3d20fc0d --- /dev/null +++ b/packages/eui/src-docs/src/views/drag_and_drop/drag_and_drop_portal.tsx @@ -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 }) => ( + + + {children} + + +); + +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 ( + <> + setFlyoutOpen(!isFlyoutOpen)}> + Toggle flyout + + + setModalOpen(!isModalOpen)}> + Toggle modal + + + {isFlyoutOpen && ( + setFlyoutOpen(false)}> + + +

+ Portalled EuiDraggable items +

+
+
+ + + {list.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content} + {state.isDragging && ' ✨'} + + )} + + ))} + + +
+ )} + + {isModalOpen && ( + setModalOpen(false)}> + + +

+ Portalled EuiDraggable items +

+
+
+ + + {list.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content} + {state.isDragging && ' ✨'} + + )} + + ))} + + +
+ )} + + + + setIsPopoverOpen(false)} + button={ + setIsPopoverOpen(!isPopoverOpen)}> + Toggle popover + + } + panelPaddingSize="none" + panelProps={{ css: { inlineSize: 200 } }} + > + { + if (source && destination) { + const items = euiDragDropReorder( + list, + source.index, + destination.index + ); + setList(items); + } + }} + > + {list.map(({ content, id }, idx) => ( + + {(provided, state) => ( + {content} + )} + + ))} + + + + ); +}; diff --git a/packages/eui/src-docs/src/views/drag_and_drop/in_popover.tsx b/packages/eui/src-docs/src/views/drag_and_drop/in_popover.tsx deleted file mode 100644 index 948808e24d0..00000000000 --- a/packages/eui/src-docs/src/views/drag_and_drop/in_popover.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState } from 'react'; -import { - EuiPopover, - EuiButton, - EuiPanel, - EuiDragDropContext, - EuiDraggable, - EuiDroppable, - euiDragDropReorder, -} from '../../../../src/components'; -import { htmlIdGenerator } from '../../../../src/services'; - -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(), - }; - }); - -export default () => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [list, setList] = useState(makeList(3)); - - return ( - setIsPopoverOpen(false)} - button={ - setIsPopoverOpen(!isPopoverOpen)}> - Toggle popover with drag and drop content - - } - panelPaddingSize="none" - panelProps={{ css: { inlineSize: 200 } }} - > - { - if (source && destination) { - const items = euiDragDropReorder( - list, - source.index, - destination.index - ); - setList(items); - } - }} - > - - {list.map(({ content, id }, idx) => ( - - {(provided, state) => ( - {content} - )} - - ))} - - - - ); -}; diff --git a/packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap b/packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap index f16b338e5ec..dbe3c0d14f1 100644 --- a/packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap +++ b/packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap @@ -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" diff --git a/packages/eui/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap b/packages/eui/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap index edd981362f8..a0f9485c137 100644 --- a/packages/eui/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap +++ b/packages/eui/src/components/datagrid/controls/__snapshots__/column_sorting.test.tsx.snap @@ -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" diff --git a/packages/eui/src/components/datagrid/controls/column_selector.styles.ts b/packages/eui/src/components/datagrid/controls/column_selector.styles.ts index 99f4c070905..762f5adfc2c 100644 --- a/packages/eui/src/components/datagrid/controls/column_selector.styles.ts +++ b/packages/eui/src/components/datagrid/controls/column_selector.styles.ts @@ -9,7 +9,11 @@ import { css } from '@emotion/react'; import { UseEuiTheme } from '../../../services'; -import { euiYScroll, logicalCSS, mathWithUnits } from '../../../global_styling'; +import { + euiYScrollWithShadows, + logicalCSS, + mathWithUnits, +} from '../../../global_styling'; import { euiShadowLarge } from '../../../themes'; export const euiDataGridColumnSelectorStyles = ( @@ -22,7 +26,7 @@ export const euiDataGridColumnSelectorStyles = ( return { euiDataGridColumnSelector: css` - ${euiYScroll(euiThemeContext)} + ${euiYScrollWithShadows(euiThemeContext)} ${logicalCSS('max-height', maxResponsiveHeight)} padding: ${euiTheme.size.s}; `, diff --git a/packages/eui/src/components/datagrid/controls/column_selector.tsx b/packages/eui/src/components/datagrid/controls/column_selector.tsx index b8a8cce4e26..3299f7574ea 100644 --- a/packages/eui/src/components/datagrid/controls/column_selector.tsx +++ b/packages/eui/src/components/datagrid/controls/column_selector.tsx @@ -144,7 +144,6 @@ export const useDataGridColumnSelector = ( closePopover={() => setIsOpen(false)} anchorPosition="downLeft" panelPaddingSize="none" - hasDragDrop button={ {(provided, state) => (
{ }); describe('sort order', () => { - const mockDrag = (handle: HTMLElement, moveEvent: Partial) => { - fireEvent.mouseDown(handle); - fireEvent.mouseMove(handle, moveEvent); - fireEvent.mouseUp(handle); - }; - it('reorders sort on drag', () => { const { getByLabelText } = render(); openPopover(); - mockDrag(getByLabelText('Drag handle'), { clientX: 0, clientY: 5 }); + // we need to re-query the handle, otherwise the handle would not + // be available because the draggable item is portalled on drag + fireEvent.mouseDown(getByLabelText('Drag handle')); + fireEvent.mouseMove(getByLabelText('Drag handle'), { + clientX: 0, + clientY: 5, + }); + fireEvent.mouseUp(getByLabelText('Drag handle')); + expect(onSort).toHaveBeenCalledWith(defaultSort); }); @@ -134,7 +136,12 @@ describe('DataGridSortingControl', () => { const { getByLabelText } = render(); openPopover(); - mockDrag(getByLabelText('Drag handle'), {}); + const draggableItem = getByLabelText('Drag handle'); + + fireEvent.mouseDown(draggableItem); + fireEvent.mouseMove(draggableItem, {}); + fireEvent.mouseUp(draggableItem); + expect(onSort).not.toHaveBeenCalled(); }); }); diff --git a/packages/eui/src/components/datagrid/controls/column_sorting.tsx b/packages/eui/src/components/datagrid/controls/column_sorting.tsx index 5d2ce573b3b..ae821ae2a27 100644 --- a/packages/eui/src/components/datagrid/controls/column_sorting.tsx +++ b/packages/eui/src/components/datagrid/controls/column_sorting.tsx @@ -179,7 +179,6 @@ export const DataGridSortingControl: FunctionComponent = closePopover={() => setIsOpen(false)} anchorPosition="downLeft" panelPaddingSize="s" - hasDragDrop button={ {(provided, state) => ( diff --git a/packages/eui/src/components/drag_and_drop/drag_drop_context.stories.tsx b/packages/eui/src/components/drag_and_drop/drag_drop_context.stories.tsx index f9e0f09918f..943fdbf5723 100644 --- a/packages/eui/src/components/drag_and_drop/drag_drop_context.stories.tsx +++ b/packages/eui/src/components/drag_and_drop/drag_drop_context.stories.tsx @@ -7,15 +7,22 @@ */ import React from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj, ReactRenderer } from '@storybook/react'; +import type { PlayFunctionContext } from '@storybook/csf'; +import { expect, fireEvent, waitFor } from '@storybook/test'; import type { DragDropContextProps } from '@hello-pangea/dnd'; import { enableFunctionToggleControls } from '../../../.storybook/utils'; +import { within } from '../../../.storybook/test'; +import { LOKI_SELECTORS } from '../../../.storybook/loki'; import { EuiPanel } from '../panel'; import { EuiDroppable } from './droppable'; import { EuiDraggable } from './draggable'; import { EuiDragDropContext } from './drag_drop_context'; +import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader } from '../flyout'; +import { EuiModal, EuiModalBody, EuiModalHeader } from '../modal'; +import { EuiTitle } from '../title'; const meta: Meta = { title: 'Display/EuiDragDropContext', @@ -25,10 +32,9 @@ const meta: Meta = { // EuiDragDropContext doesn't do anything visual, we're testing the // visual parts with the Drag and Drop components separately skip: true, - }, - codeSnippet: { - // TODO: enable once render functions are supported - skip: true, + codeSnippet: { + resolveStoryElementOnly: true, + }, }, }, }; @@ -64,3 +70,117 @@ export const Playground: Story = { ), }, }; + +export const WithinFlyouts: Story = { + tags: ['vrt-only'], + parameters: { + loki: { + skip: false, + chromeSelector: LOKI_SELECTORS.portal, + }, + }, + args: { + children: ( + + + {(_, state) => ( + + Draggable item 1 {state.isDragging && '✨'} + + )} + + + {(_, state) => ( + + Draggable item 2 {state.isDragging && '✨'} + + )} + + + ), + }, + render: (args) => , + play: async ({ canvasElement }: PlayFunctionContext) => { + const canvas = within(canvasElement); + + await waitFor(async () => { + expect(canvas.getByRole('dialog')).toBeInTheDocument(); + expect(canvas.getByRole('dialog')).toBeVisible(); + }); + + await setTimeout(async () => { + await waitFor(async () => { + await fireEvent.mouseDown(canvas.getByTestSubject('draggable-item-1')); + await fireEvent.mouseMove(canvas.getByTestSubject('draggable-item-1'), { + clientX: 0, + clientY: 5, + }); + + expect( + [...canvas.getByTestSubject('draggable-item-1').classList] + .join('') + .includes('isDragging') + ).toBe(true); + }); + }, 150); // add a timeout to prevent differences due to animation + }, +}; + +export const WithinModals: Story = { + tags: ['vrt-only'], + ...WithinFlyouts, + render: (args) => , +}; + +const VRTStory = ({ + type, + ...args +}: DragDropContextProps & { type: 'flyout' | 'modal' }) => { + if (type === 'flyout') { + return ( + {}} data-test-subj="flyoutDragDrop"> + + +

Drag & Drop inside a flyout

+
+
+ + + + + +
+ ); + } + + if (type === 'modal') { + return ( + {}}> + + +

Drag & Drop inside a modal

+
+
+ + + + + +
+ ); + } + + return null; +}; diff --git a/packages/eui/src/components/drag_and_drop/draggable.stories.tsx b/packages/eui/src/components/drag_and_drop/draggable.stories.tsx index b8a08fca957..b2457e3435f 100644 --- a/packages/eui/src/components/drag_and_drop/draggable.stories.tsx +++ b/packages/eui/src/components/drag_and_drop/draggable.stories.tsx @@ -52,6 +52,7 @@ const meta: Meta = { hasInteractiveChildren: false, isRemovable: false, spacing: 'none', + usePortal: false, }, }; hideStorybookControls(meta, ['style']); diff --git a/packages/eui/src/components/drag_and_drop/draggable.tsx b/packages/eui/src/components/drag_and_drop/draggable.tsx index a9fb66dacc5..1a45309cd3c 100644 --- a/packages/eui/src/components/drag_and_drop/draggable.tsx +++ b/packages/eui/src/components/drag_and_drop/draggable.tsx @@ -20,6 +20,7 @@ import { CommonProps } from '../common'; import { EuiDroppableContext, SPACINGS } from './droppable'; import { euiDraggableStyles, euiDraggableItemStyles } from './draggable.styles'; +import { EuiPortal } from '../portal'; export interface EuiDraggableProps extends CommonProps, @@ -42,6 +43,15 @@ export interface EuiDraggableProps * Whether the item is currently in a position to be removed */ isRemovable?: boolean; + /** + * Whether the currently dragged item is cloned into a portal in the body. This settings will + * ensure that drag & drop still works as expected within stacking contexts (e.g. within `EuiFlyout`, + * `EuiModal` and `EuiPopover`). + * + * Make sure to apply styles directly to the Draggable content as relative styling from an outside + * scope might not be applied when the content is placed in a portal as the DOM structure changes. + */ + usePortal?: boolean; /** * Adds padding to the draggable item */ @@ -55,6 +65,7 @@ export const EuiDraggable: FunctionComponent = ({ isDragDisabled = false, hasInteractiveChildren = false, isRemovable = false, + usePortal = false, index, children, className, @@ -94,7 +105,8 @@ export const EuiDraggable: FunctionComponent = ({ typeof children === 'function' ? (children(provided, snapshot, rubric) as ReactElement) : children; - return ( + + const content = ( <>
= ({ data-test-subj={dataTestSubj} className={classes} css={cssStyles} - style={{ ...style, ...provided.draggableProps.style }} + style={{ + ...style, + ...provided.draggableProps.style, + }} // We use [role="group"] instead of [role="button"] when we expect a nested // interactive element. Screen readers will cue users that this is a container // and has one or more elements inside that are part of a related group. @@ -141,6 +156,12 @@ export const EuiDraggable: FunctionComponent = ({ )} ); + + return isDragging && usePortal ? ( + {content} + ) : ( + content + ); }} ); diff --git a/packages/eui/src/components/popover/popover.tsx b/packages/eui/src/components/popover/popover.tsx index 78c13b94226..d78b25eb1dd 100644 --- a/packages/eui/src/components/popover/popover.tsx +++ b/packages/eui/src/components/popover/popover.tsx @@ -177,6 +177,8 @@ export interface EuiPopoverProps extends PropsWithChildren, CommonProps { /** * Must be set to true if using `EuiDragDropContext` within a popover, * otherwise your nested drag & drop will have incorrect positioning + * + * @deprecated - use `usePortal` prop on children `EuiDraggable` components instead. */ hasDragDrop?: boolean; /** diff --git a/packages/website/docs/02_components/display/drag_and_drop/overview.mdx b/packages/website/docs/02_components/display/drag_and_drop/overview.mdx index 38f1890e25b..33597d24723 100644 --- a/packages/website/docs/02_components/display/drag_and_drop/overview.mdx +++ b/packages/website/docs/02_components/display/drag_and_drop/overview.mdx @@ -126,20 +126,202 @@ the visual changes with drop-to-remove interactions. -## Kitchen sink - -**EuiDraggables** in **EuiDroppables**, **EuiDroppables** in **EuiDraggables**, custom drag handles, horizontal -movement, vertical movement, flexbox, panel inception, you name it. - - - -## Using drag and drop in popovers +## Portalled items **EuiDraggables** use fixed positioning to render and animate the item being dragged. This positioning logic does not work as expected when used inside of containers that have their own [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context). -This behavior particularly affects [**EuiPopover**](#/layout/popover). If using drag and drop UX within a popover, -you **must** include the `` prop for items to properly render while being dragged. +To ensure dragging works as expected inside e.g. **EuiFlyout** or **EuiModal** use the prop `usePortal` on **EuiDraggable** components. +This will render the currently dragged element inside a portal appended to the document body (or wherever +**EuiPortal** is configured to `insert` to by default). + +:::warning +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 `usePortal`. 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 **EuiDraggable** scope without any parent selectors. +::: + +```tsx interactive +import React, { FunctionComponent, ReactElement, useState } from 'react'; +import { + EuiButton, + EuiCode, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiPanel, + EuiSpacer, + EuiTitle, + euiDragDropReorder, + htmlIdGenerator +} from '@elastic/eui'; +import { DroppableProps, OnDragEndResponder } from '@hello-pangea/dnd'; + +const makeId = htmlIdGenerator(); + +const makeList = (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 }) => ( + + + {children} + + +); + +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 = ({ source, destination }) => { + if (source && destination) { + const items = euiDragDropReorder(list, source.index, destination.index); + + setList(items); + } + }; + + return ( + <> + setFlyoutOpen(!isFlyoutOpen)}> + Toggle flyout + + + setModalOpen(!isModalOpen)}> + Toggle modal + + + {isFlyoutOpen && ( + setFlyoutOpen(false)}> + + +

+ Portalled EuiDraggable items +

+
+
+ + + {list.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content} + {state.isDragging && ' ✨'} + + )} + + ))} + + +
+ )} + + {isModalOpen && ( + setModalOpen(false)}> + + +

+ Portalled EuiDraggable items +

+
+
+ + + {list.map(({ content, id }, idx) => ( + + {(provided, state) => ( + + {content} + {state.isDragging && ' ✨'} + + )} + + ))} + + +
+ )} + + + + setIsPopoverOpen(false)} + button={ + setIsPopoverOpen(!isPopoverOpen)}> + Toggle popover + + } + panelPaddingSize="none" + panelProps={{ css: { inlineSize: 200 } }} + > + { + if (source && destination) { + const items = euiDragDropReorder( + list, + source.index, + destination.index + ); + setList(items); + } + }} + > + {list.map(({ content, id }, idx) => ( + + {(provided, state) => ( + {content} + )} + + ))} + + + + ); +}; +``` + +## Kitchen sink - +**EuiDraggables** in **EuiDroppables**, **EuiDroppables** in **EuiDraggables**, custom drag handles, horizontal +movement, vertical movement, flexbox, panel inception, you name it. + +