Skip to content

Commit

Permalink
[lens][draft] Lens/drag drop (#36268)
Browse files Browse the repository at this point in the history
Add basic drag / drop component to Lens
  • Loading branch information
chrisdavies authored May 10, 2019
1 parent abc6f04 commit 0dfd683
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 0 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions x-pack/plugins/lens/public/drag_drop/drag_drop.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.lnsDragDrop-isDropTarget {
background-color: transparentize($euiColorSecondary, .9);
}

.lnsDragDrop-isActiveDropTarget {
background-color: transparentize($euiColorSecondary, .75);
}
104 changes: 104 additions & 0 deletions x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { render, shallow, mount } from 'enzyme';
import { DragDrop } from './drag_drop';
import { ChildDragDropProvider } from './providers';

jest.useFakeTimers();

describe('DragDrop', () => {
test('renders if nothing is being dragged', () => {
const component = render(
<DragDrop value="hello" draggable>
Hello!
</DragDrop>
);

expect(component).toMatchSnapshot();
});

test('dragover calls preventDefault if droppable is true', () => {
const preventDefault = jest.fn();
const component = shallow(<DragDrop droppable>Hello!</DragDrop>);

component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault });

expect(preventDefault).toBeCalled();
});

test('dragover does not call preventDefault if droppable is false', () => {
const preventDefault = jest.fn();
const component = shallow(<DragDrop>Hello!</DragDrop>);

component.find('[data-test-subj="lnsDragDrop"]').simulate('dragover', { preventDefault });

expect(preventDefault).not.toBeCalled();
});

test('dragstart sets dragging in the context', async () => {
const setDragging = jest.fn();
const dataTransfer = {
setData: jest.fn(),
getData: jest.fn(),
};
const value = {};

const component = mount(
<ChildDragDropProvider dragging={undefined} setDragging={setDragging}>
<DragDrop value={value}>Hello!</DragDrop>
</ChildDragDropProvider>
);

component.find('[data-test-subj="lnsDragDrop"]').simulate('dragstart', { dataTransfer });

jest.runAllTimers();

expect(dataTransfer.setData).toBeCalledWith('text', 'dragging');
expect(setDragging).toBeCalledWith(value);
});

test('drop resets all the things', async () => {
const preventDefault = jest.fn();
const stopPropagation = jest.fn();
const setDragging = jest.fn();
const onDrop = jest.fn();
const value = {};

const component = mount(
<ChildDragDropProvider dragging="hola" setDragging={setDragging}>
<DragDrop onDrop={onDrop} value={value}>
Hello!
</DragDrop>
</ChildDragDropProvider>
);

component
.find('[data-test-subj="lnsDragDrop"]')
.simulate('drop', { preventDefault, stopPropagation });

expect(preventDefault).toBeCalled();
expect(stopPropagation).toBeCalled();
expect(setDragging).toBeCalledWith(undefined);
expect(onDrop).toBeCalledWith('hola');
});

test('droppable is reflected in the className', () => {
const component = render(
<DragDrop
onDrop={(x: any) => {
throw x;
}}
droppable
>
Hello!
</DragDrop>
);

expect(component).toMatchSnapshot();
});
});
138 changes: 138 additions & 0 deletions x-pack/plugins/lens/public/drag_drop/drag_drop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useState, useContext } from 'react';
import classNames from 'classnames';
import { DragContext } from './providers';

type DroppableEvent = React.DragEvent<HTMLElement>;

/**
* A function that handles a drop event.
*/
type DropHandler = (item: unknown) => void;

/**
* The argument to the DragDrop component.
*/
interface Props {
/**
* The CSS class(es) for the root element.
*/
className?: string;

/**
* The event handler that fires when an item
* is dropped onto this DragDrop component.
*/
onDrop?: DropHandler;

/**
* The value associated with this item, if it is draggable.
* If this component is dragged, this will be the value of
* "dragging" in the root drag/drop context.
*/
value?: unknown;

/**
* The React children.
*/
children: React.ReactNode;

/**
* Indicates whether or not the currently dragged item
* can be dropped onto this component.
*/
droppable?: boolean;

/**
* Indicates whether or not this component is draggable.
*/
draggable?: boolean;
}

/**
* A draggable / droppable item. Items can be both draggable and droppable at
* the same time.
*
* @param props
*/
export function DragDrop(props: Props) {
const { dragging, setDragging } = useContext(DragContext);
const [state, setState] = useState({ isActive: false });
const { className, onDrop, value, children, droppable, draggable } = props;
const isDragging = draggable && value === dragging;

const classes = classNames('lnsDragDrop', className, {
'lnsDragDrop-isDropTarget': droppable,
'lnsDragDrop-isActiveDropTarget': droppable && state.isActive,
'lnsDragDrop-isDragging': isDragging,
});

const dragStart = (e: DroppableEvent) => {
// Setting stopPropgagation causes Chrome failures, so
// we are manually checking if we've already handled this
// in a nested child, and doing nothing if so...
if (e.dataTransfer.getData('text')) {
return;
}

e.dataTransfer.setData('text', 'dragging');

// Chrome causes issues if you try to render from within a
// dragStart event, so we drop a setTimeout to avoid that.
setTimeout(() => setDragging(value));
};

const dragEnd = (e: DroppableEvent) => {
e.stopPropagation();
setDragging(undefined);
};

const dragOver = (e: DroppableEvent) => {
if (!droppable) {
return;
}

e.preventDefault();

// An optimization to prevent a bunch of React churn.
if (!state.isActive) {
setState({ ...state, isActive: true });
}
};

const dragLeave = () => {
setState({ ...state, isActive: false });
};

const drop = (e: DroppableEvent) => {
e.preventDefault();
e.stopPropagation();

setState({ ...state, isActive: false });
setDragging(undefined);

if (onDrop) {
onDrop(dragging);
}
};

return (
<div
data-test-subj="lnsDragDrop"
className={classes}
onDragOver={dragOver}
onDragLeave={dragLeave}
onDrop={drop}
draggable={draggable}
onDragEnd={dragEnd}
onDragStart={dragStart}
>
{children}
</div>
);
}
8 changes: 8 additions & 0 deletions x-pack/plugins/lens/public/drag_drop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export * from './providers';
export * from './drag_drop';
85 changes: 85 additions & 0 deletions x-pack/plugins/lens/public/drag_drop/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useState } from 'react';

/**
* The shape of the drag / drop context.
*/
export interface DragContextState {
/**
* The item being dragged or undefined.
*/
dragging: unknown;

/**
* Set the item being dragged.
*/
setDragging: (dragging: unknown) => void;
}

/**
* The drag / drop context singleton, used like so:
*
* const { dragging, setDragging } = useContext(DragContext);
*/
export const DragContext = React.createContext<DragContextState>({
dragging: undefined,
setDragging: () => {},
});

/**
* The argument to DragDropProvider.
*/
export interface ProviderProps {
/**
* The item being dragged. If unspecified, the provider will
* behave as if it is the root provider.
*/
dragging: unknown;

/**
* Sets the item being dragged. If unspecified, the provider
* will behave as if it is the root provider.
*/
setDragging: (dragging: unknown) => void;

/**
* The React children.
*/
children: React.ReactNode;
}

/**
* A React provider that tracks the dragging state. This should
* be placed at the root of any React application that supports
* drag / drop.
*
* @param props
*/
export function RootDragDropProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<{ dragging: unknown }>({
dragging: undefined,
});
const setDragging = (dragging: unknown) => setState({ dragging });

return (
<ChildDragDropProvider dragging={state.dragging} setDragging={setDragging}>
{children}
</ChildDragDropProvider>
);
}

/**
* A React drag / drop provider that derives its state from a RootDragDropProvider. If
* part of a React application is rendered separately from the root, this provider can
* be used to enable drag / drop functionality within the disconnected part.
*
* @param props
*/
export function ChildDragDropProvider({ dragging, setDragging, children }: ProviderProps) {
return <DragContext.Provider value={{ dragging, setDragging }}>{children}</DragContext.Provider>;
}
Loading

0 comments on commit 0dfd683

Please sign in to comment.