Skip to content

Commit

Permalink
Merge pull request #1517 from clauderic/iframes-support
Browse files Browse the repository at this point in the history
Add same-origin iframe support
  • Loading branch information
clauderic authored Nov 4, 2024
2 parents 52ddf99 + 9134a41 commit 412ab76
Show file tree
Hide file tree
Showing 25 changed files with 529 additions and 103 deletions.
5 changes: 5 additions & 0 deletions .changeset/support-same-origin-iframes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dnd-kit/dom': patch
---

Support dragging across same-origin iframes.
9 changes: 8 additions & 1 deletion apps/stories/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ const preview = {
'Draggable',
'Droppable',
'Sortable',
['Vertical list', 'Horizontal list', 'Grid'],
[
'Vertical list',
'Horizontal list',
'Grid',
'Multiple lists',
'Iframe',
'Virtualized',
],
],
],
},
Expand Down
1 change: 1 addition & 0 deletions apps/stories/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"devDependencies": {
"@dnd-kit/eslint-config": "*",
"@measured/auto-frame-component": "^0.1.7",
"@storybook/addon-actions": "^8.4.1",
"@storybook/addon-docs": "^8.4.1",
"@storybook/addon-essentials": "^8.4.1",
Expand Down
28 changes: 28 additions & 0 deletions apps/stories/stories/react/Sortable/Iframe/Iframe.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {Meta, StoryObj} from '@storybook/react';

import {IframeLists} from './IframeExample.tsx';

const meta: Meta<typeof IframeLists> = {
title: 'React/Sortable/Iframe',
component: IframeLists,
};

export default meta;
type Story = StoryObj<typeof IframeLists>;

export const Iframe: Story = {
name: 'Iframe',
args: {
debug: false,
itemCount: 6,
},
};

export const IframeTransformed: Story = {
name: 'Transformed Iframe',
args: {
debug: false,
itemCount: 6,
transform: true,
},
};
170 changes: 170 additions & 0 deletions apps/stories/stories/react/Sortable/Iframe/IframeExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React, {useEffect, useRef, useState} from 'react';
import type {PropsWithChildren} from 'react';
import {DragDropProvider} from '@dnd-kit/react';
import {useSortable} from '@dnd-kit/react/sortable';
import {move} from '@dnd-kit/helpers';
import {defaultPreset} from '@dnd-kit/dom';
import {Debug} from '@dnd-kit/dom/plugins/debug';
import AutoFrameComponent from '@measured/auto-frame-component';

import {Container, Item} from '../../components/index.ts';
import {createRange} from '../../../utilities/createRange.ts';
import {cloneDeep} from '../../../utilities/cloneDeep.ts';

const AutoFrame = AutoFrameComponent.default || AutoFrameComponent;

interface Props {
debug?: boolean;
defaultItems?: Record<string, string[]>;
columnStyle?: Record<string, string>;
itemCount: number;
scrollable?: boolean;
transform?: boolean;
}

export function IframeLists({
debug,
defaultItems,
itemCount,
columnStyle,
scrollable,
transform,
}: Props) {
const [items, setItems] = useState(
defaultItems ?? {
host: createRange(itemCount).map((id) => `Host: ${id}`),
iframe: createRange(itemCount).map((id) => `Iframe: ${id}`),
}
);
const snapshot = useRef(cloneDeep(items));

const [bodyClassName, setBodyClassName] = useState('');

useEffect(() => {
const body = document.querySelector('body');

if (!body) return;

if (body.classList.contains('dark')) {
setBodyClassName('dark');
}
}, []);

return (
<DragDropProvider
plugins={debug ? [...defaultPreset.plugins, Debug] : undefined}
onDragStart={() => {
snapshot.current = cloneDeep(items);
}}
onDragOver={(event) => {
setItems((items) => move(items, event));
}}
onDragEnd={(event) => {
if (event.canceled) {
setItems(snapshot.current);
return;
}
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
gap: 20,
}}
>
<Column id="host" scrollable={scrollable} style={columnStyle}>
{items.host.map((id, index) => (
<SortableItem key={id} id={id} column={'host'} index={index} />
))}
</Column>

<AutoFrame
style={{
border: 'none',
transform: transform ? 'scale(0.8)' : undefined,
}}
>
<style
dangerouslySetInnerHTML={{
__html: 'body { background: transparent; margin: 0 !important; }',
}}
/>
<div className={bodyClassName}>
<Column id="iframe" scrollable={scrollable} style={columnStyle}>
{items.iframe.map((id, index) => (
<SortableItem key={id} id={id} column="iframe" index={index} />
))}
</Column>
</div>
</AutoFrame>
</div>
</DragDropProvider>
);
}

interface SortableItemProps {
id: string;
column: string;
index: number;
style?: React.CSSProperties;
}

const COLORS: Record<string, string> = {
Host: '#7193f1',
Iframe: '#FF851B',
};

function SortableItem({
id,
column,
index,
style,
}: PropsWithChildren<SortableItemProps>) {
const group = column;
const {ref, isDragSource} = useSortable({
id,
group,
accept: 'item',
type: 'item',
feedback: 'clone',
index,
data: {group},
});

return (
<Item
ref={ref}
accentColor={COLORS[column]}
shadow={isDragSource}
style={style}
transitionId={`sortable-${column}-${id}`}
>
{id}
</Item>
);
}

interface ColumnProps {
id: string;
scrollable?: boolean;
style?: React.CSSProperties;
}

function Column({
children,
id,
scrollable,
style,
}: PropsWithChildren<ColumnProps>) {
return (
<Container
label={id.charAt(0).toUpperCase() + id.slice(1)}
scrollable={scrollable}
transitionId={`sortable-column-${id}`}
style={style}
>
{children}
</Container>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

.Header {
display: flex;
min-height: 59px;
padding: 8px 20px;
padding-right: 8px;
align-items: center;
Expand Down
Binary file modified bun.lockb
Binary file not shown.
4 changes: 3 additions & 1 deletion packages/dom/src/core/entities/droppable/droppable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ export class Droppable<T extends Data = Data> extends AbstractDroppable<
) {
const {collisionDetector = defaultCollisionDetection} = input;
const updateShape = (boundingClientRect?: BoundingRectangle | null) => {
const {element} = this;
const {manager, element} = this;

if (!element || boundingClientRect === null) {
this.shape = undefined;
return undefined;
}

if (!manager) return;

const updatedShape = new DOMRectangle(element);

const shape = untracked(() => this.shape);
Expand Down
8 changes: 6 additions & 2 deletions packages/dom/src/core/manager/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
type Plugins,
type Sensors,
} from '@dnd-kit/abstract';
import {isElement} from '@dnd-kit/dom/utilities';
import {
DOMRectangle,
type DOMRectangleOptions,
isElement,
} from '@dnd-kit/dom/utilities';

import type {Draggable, Droppable} from '../entities/index.ts';
import {
Expand All @@ -18,7 +22,7 @@ import {
} from '../plugins/index.ts';
import {KeyboardSensor, PointerSensor} from '../sensors/index.ts';

export interface Input extends DragDropManagerInput<any> {}
export interface Input extends DragDropManagerInput<DragDropManager> {}

export const defaultPreset: {
plugins: Plugins<DragDropManager>;
Expand Down
Loading

0 comments on commit 412ab76

Please sign in to comment.