Skip to content

Commit

Permalink
Add new usePreview hook in react-dnd-multi-backend
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisBrunner committed May 1, 2020
1 parent 95a7652 commit 100602a
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 43 deletions.
3 changes: 3 additions & 0 deletions packages/dnd-multi-backend/src/__tests__/index_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import MultiBackend from '../MultiBackend';

import { HTML5DragTransition, TouchTransition, MouseTransition } from '../Transitions';
import createTransition from '../createTransition';
import { PreviewList, PreviewManager } from '../PreviewList';


describe('ReactDnDMultiBackend module', () => {
Expand All @@ -21,5 +22,7 @@ describe('ReactDnDMultiBackend module', () => {
expect(Module.TouchTransition).toBe(TouchTransition);
expect(Module.MouseTransition).toBe(MouseTransition);
expect(Module.createTransition).toBe(createTransition);
expect(Module.PreviewList).toBe(PreviewList);
expect(Module.PreviewManager).toBe(PreviewManager);
});
});
18 changes: 18 additions & 0 deletions packages/react-dnd-multi-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,24 @@ Both of them receive the same data formatted the same way, an object containing

Note that this component will only be showed while using a backend flagged with `preview: true` (see [Create a custom pipeline](#create-a-custom-pipeline)) which is the case for the Touch backend in the default `HTML5toTouch` pipeline.

#### Context-based

```js
import MultiBackend, { usePreview } from 'react-dnd-multi-backend';
...
const MyPreview = () => {
const {display, itemType, item, style} = usePreview();
if (!display) {
return null;
}
// render your preview
};
...
<Preview>
<MyPreview />
</Preview>
```

#### Function-based

```js
Expand Down
73 changes: 51 additions & 22 deletions packages/react-dnd-multi-backend/examples/App.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,58 @@
import React, { useContext, useRef, useState } from 'react';
import { DndProvider as ReactDndProvider } from 'react-dnd';
import MultiBackend, { DndProvider, Preview } from '../src';
import MultiBackend, { DndProvider, PreviewContext, usePreview, Preview } from '../src';
import HTML5toTouch from '../src/HTML5toTouch';
import Card from './Card';
import Basket from './Basket';

const GeneratePreview = ({text}) => { // eslint-disable-line react/prop-types
const {style, item} = useContext(Preview.Context);
return <div style={{...style, backgroundColor: item.color, width: '50px', height: '50px'}}>Generated {text}</div>;
const generatePreview = (row, text, item, style) => {
return <div style={{
...style,
top: `${row * 60}px`,
backgroundColor: item.color,
width: '50px',
height: '50px',
whiteSpace: 'nowrap',
}}>Generated {text}</div>;
};

const ContextPreview = ({text}) => { // eslint-disable-line react/prop-types
const {style, item} = useContext(PreviewContext);
return generatePreview(0, `${text} with Context`, item, style);
};

const HookPreview = ({text}) => { // eslint-disable-line react/prop-types
const {display, style, item} = usePreview();
if (!display) {
return null;
}
return generatePreview(1, `${text} with Hook`, item, style);
};

const ComponentPreview = ({text}) => { // eslint-disable-line react/prop-types
return (
<Preview generator={({item, style}) => {
return generatePreview(2, `${text} with Component`, item, style);
}} />
);
};

const getContent = (title, ref) => {
return (
<>
<h1>{title} API</h1>
<Card color="#cc2211" />
<Card color="#22cc11" />
<Card color="#2211cc" />
<Basket logs={ref} />
<Preview>
<ContextPreview text={title} />
</Preview>
<HookPreview text={title} />
<ComponentPreview text={title} />
<div ref={ref} />
</>
);
};

const App = () => {
Expand All @@ -18,29 +63,13 @@ const App = () => {

const oldAPI = (
<ReactDndProvider backend={MultiBackend} options={HTML5toTouch}>
<h1>Old API</h1>
<Card color="#cc2211" />
<Card color="#22cc11" />
<Card color="#2211cc" />
<Basket logs={refOld} />
<Preview>
<GeneratePreview text="old" />
</Preview>
<div ref={refOld} />
{getContent('Old', refOld)}
</ReactDndProvider>
);

const newAPI = (
<DndProvider options={HTML5toTouch}>
<h1>New API</h1>
<Card color="#cc2211" />
<Card color="#22cc11" />
<Card color="#2211cc" />
<Basket logs={refNew} />
<Preview>
<GeneratePreview text="new" />
</Preview>
<div ref={refNew} />
{getContent('New', refNew)}
</DndProvider>
);

Expand Down
23 changes: 5 additions & 18 deletions packages/react-dnd-multi-backend/src/Preview.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
import React, { useState, useEffect, useContext } from 'react';
import React, { useContext } from 'react';
import ReactDOM from 'react-dom';
import DnDPreview, { Context as PreviewContext } from 'react-dnd-preview';
import { PreviewsContext, PreviewPortalContext } from './DndProvider';

import { useObservePreviews } from './commonPreview';
import { PreviewPortalContext } from './DndProvider';

const Preview = (props) => {
const [enabled, setEnabled] = useState(false);
const previews = useContext(PreviewsContext);
const enabled = useObservePreviews();
const portal = useContext(PreviewPortalContext);

useEffect(() => {
const observer = {
backendChanged: (backend) => {
setEnabled(backend.previewEnabled());
},
};

previews.register(observer);
return () => {
previews.unregister(observer);
};
}, [previews, setEnabled]);

if (!enabled) {
return null;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/react-dnd-multi-backend/src/__tests__/index_spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as Module from '../index';

import { Preview, PreviewContext } from '../Preview';
import { HTML5DragTransition, TouchTransition, MouseTransition, createTransition } from 'dnd-multi-backend';
import { usePreview } from '../usePreview';
import MultiBackend, { HTML5DragTransition, TouchTransition, MouseTransition, createTransition } from 'dnd-multi-backend';


describe('ReactDnDMultiBackend module', () => {
Expand All @@ -14,8 +15,10 @@ describe('ReactDnDMultiBackend module', () => {
});

test('exports utils components', () => {
expect(Module.default).toBe(MultiBackend);
expect(Module.Preview).toBe(Preview);
expect(Module.PreviewContext).toBe(PreviewContext);
expect(Module.usePreview).toBe(usePreview);
expect(Module.HTML5DragTransition).toBe(HTML5DragTransition);
expect(Module.TouchTransition).toBe(TouchTransition);
expect(Module.MouseTransition).toBe(MouseTransition);
Expand Down
161 changes: 161 additions & 0 deletions packages/react-dnd-multi-backend/src/__tests__/usePreview_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';

import { Preview, PreviewContext } from '../Preview';
import { PreviewManager, PreviewList } from 'dnd-multi-backend';
import { wrapInTestContext } from 'react-dnd-test-utils';
import { PreviewPortalContext, PreviewsContext } from '../DndProvider';

const setupTest = (create, lazyList) => {
let list;

beforeEach(() => {
list = lazyList();
});

const getLastRegister = () => {
return list.register.mock.calls[list.register.mock.calls.length - 1][0];
};

test('registers with the backend', () => {
expect(list.register).not.toBeCalled();
const component = create();
const matcher = expect.objectContaining({backendChanged: expect.any(Function)});
expect(list.register).toBeCalledWith(matcher);
expect(list.unregister).not.toBeCalled();
component.unmount();
expect(list.unregister).toBeCalledWith(matcher);
});

test('is empty (no preview)', () => {
const component = create();
expect(component.find(Preview).html()).toBeNull();
});

test('is not empty (preview)', () => {
const component = create({
generator: () => { // eslint-disable-line react/display-name
return <div>abc</div>;
},
});
expect(component.find(Preview).html()).toBeNull();

act(() => {
getLastRegister().backendChanged({
previewEnabled: () => true,
});
});
component.update();
expect(component.find(Preview).html()).not.toBeNull();

act(() => {
getLastRegister().backendChanged({
previewEnabled: () => false,
});
});
component.update();
expect(component.find(Preview).html()).toBeNull();
});

return getLastRegister;
};

describe('Preview component', () => {
test('exports a context', () => {
expect(Preview.Context).toBe(PreviewContext);
});

describe('using global context', () => {
beforeEach(() => {
jest.spyOn(PreviewManager, 'register');
jest.spyOn(PreviewManager, 'unregister');
});

afterEach(() => {
PreviewManager.register.mockRestore();
PreviewManager.unregister.mockRestore();
});

const createComponent = ({generator = jest.fn()} = {}) => {
const Wrapped = wrapInTestContext(Preview);
return mount(<Wrapped generator={generator} />);
};

setupTest(createComponent, () => PreviewManager);
});

describe('using previews context', () => {
let list;

beforeEach(() => {
list = new PreviewList();
jest.spyOn(list, 'register');
jest.spyOn(list, 'unregister');
});

const createComponent = ({generator = jest.fn()} = {}) => {
const Wrapped = wrapInTestContext(Preview);
return mount(
<PreviewsContext.Provider value={list}>
<Wrapped generator={generator} />
</PreviewsContext.Provider>
);
};

setupTest(createComponent, () => list);
});

describe('using previews and portal context', () => {
let list;

beforeEach(() => {
list = new PreviewList();
jest.spyOn(list, 'register');
jest.spyOn(list, 'unregister');
});

const createComponent = ({generator = jest.fn()} = {}) => {
const Wrapped = wrapInTestContext(Preview);
const Component = class Root extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}

render() {
return (
<>
<PreviewPortalContext.Provider value={this.ref.current}>
<PreviewsContext.Provider value={list}>
<Wrapped generator={generator} />
</PreviewsContext.Provider>
</PreviewPortalContext.Provider>
<div ref={this.ref} />
</>
);
}
};
const component = mount(<Component />);
component.instance().forceUpdate();
return component;
};

const getLastRegister = setupTest(createComponent, () => list);

test('portal is in detached div', () => {
const component = createComponent({
generator: () => { // eslint-disable-line react/display-name
return <span>123</span>;
},
});
act(() => {
getLastRegister().backendChanged({
previewEnabled: () => true,
});
});
component.update();
expect(component.find(Preview).find('Portal').html()).not.toBeNull();
});
});
});
25 changes: 25 additions & 0 deletions packages/react-dnd-multi-backend/src/commonPreview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState, useEffect, useContext } from 'react';

import { PreviewsContext } from './DndProvider';

const useObservePreviews = () => {
const [enabled, setEnabled] = useState(false);
const previews = useContext(PreviewsContext);

useEffect(() => {
const observer = {
backendChanged: (backend) => {
setEnabled(backend.previewEnabled());
},
};

previews.register(observer);
return () => {
previews.unregister(observer);
};
}, [previews, setEnabled]);

return enabled;
};

export { useObservePreviews };
1 change: 1 addition & 0 deletions packages/react-dnd-multi-backend/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default, HTML5DragTransition, TouchTransition, MouseTransition, createTransition } from 'dnd-multi-backend';
export { DndProvider } from './DndProvider';
export { Preview, PreviewContext } from './Preview';
export { usePreview } from './usePreview';
15 changes: 15 additions & 0 deletions packages/react-dnd-multi-backend/src/usePreview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { usePreview as usePreviewDnd } from 'react-dnd-preview';

import { useObservePreviews } from './commonPreview';

const usePreview = () => {
const enabled = useObservePreviews();
const result = usePreviewDnd();
if (!enabled) {
return {display: false};
}

return result;
};

export { usePreview };

0 comments on commit 100602a

Please sign in to comment.