Skip to content

Commit

Permalink
feat(preview): allow changing the preview placement (#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
MPeloquin authored Dec 12, 2024
1 parent 6cc2783 commit 2c8140a
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 74 deletions.
6 changes: 3 additions & 3 deletions packages/react-dnd-preview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ See also the [examples](examples/) for more information.
import { usePreview } from 'react-dnd-preview'

const MyPreview = () => {
const preview = usePreview()
const preview = usePreview({ placement: 'top', padding: {x: -20, y: 0 }})
if (!preview.display) {
return null
}
const {itemType, item, style} = preview;
return <div className="item-list__item" style={style}>{itemType}</div>
const {itemType, item, style, ref} = preview;
return <div className="item-list__item" ref={ref} style={style}>{itemType}</div>
}

const App = () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/react-dnd-preview/examples/main/methods/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export type GenPreviewProps = {
method: string,
}

export const generatePreview = ({itemType, item, style}: PreviewProps, {row, col, title, method}: GenPreviewProps): JSX.Element => {
export const generatePreview = ({itemType, item, style, ref}: PreviewProps, {row, col, title, method}: GenPreviewProps): JSX.Element => {
return (
<Shape color={item.color} size={50} style={{
<Shape color={item.color} size={50} ref={ref} style={{
...style,
top: `${row * 60}px`,
left: `${col * 100}px`,
Expand Down
60 changes: 51 additions & 9 deletions packages/react-dnd-preview/examples/offset/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React, {CSSProperties, useState} from 'react'
import {DndProvider} from 'react-dnd'
import {TouchBackend} from 'react-dnd-touch-backend'
import {usePreview} from '../../src'
import {usePreview, Point} from '../../src'
import type {PreviewPlacement} from '../../src/'
import {Draggable, Shape, DragContent} from '../shared'

type Kinds = 'default' | 'ref' | 'custom_client' | 'custom_source_client'

type PreviewProps = {
kind: Kinds,
text: string,
placement?: PreviewPlacement,
padding?: Point
}

export const Preview = ({kind, text}: PreviewProps): JSX.Element | null => {
const preview = usePreview<DragContent, HTMLDivElement>()
export const Preview = ({kind, text, placement, padding}: PreviewProps): JSX.Element | null => {
const preview = usePreview<DragContent, HTMLDivElement>({placement, padding})
if (!preview.display) {
return null
}
Expand Down Expand Up @@ -47,23 +50,62 @@ export const Preview = ({kind, text}: PreviewProps): JSX.Element | null => {

export const App = (): JSX.Element => {
const [debug, setDebug] = useState(false)
const [previewPlacement, setPreviewPlacement] = useState<PreviewPlacement>('center')

const [paddingX, setPaddingX] = useState('0')
const [paddingY, setPaddingY] = useState('0')

const handlePlacementChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setPreviewPlacement(e.target.value as PreviewPlacement)
}

const handlePaddingXChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPaddingX(e.target.value)
}

const handlePaddingYChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPaddingY(e.target.value)
}

return (
<React.StrictMode>
<p>
<label htmlFor="previewPlacement">Preview placement: </label>
<select value={previewPlacement} id="previewPlacement" onChange={handlePlacementChange}>
<option value="center">center (default)</option>
<option value="top-start">top-start</option>
<option value="top">top</option>
<option value="top-end">top-end</option>
<option value="bottom-start">bottom-start</option>
<option value="bottom">bottom</option>
<option value="bottom-end">bottom-end</option>
<option value="left">left</option>
<option value="right">right</option>
</select>
</p>
<p>
<label htmlFor="previewPlacement">Padding x: </label>
<input type="text" value={paddingX} onChange={handlePaddingXChange}/>
</p>
<p>
<label htmlFor="previewPlacement">Padding y: </label>
<input type="text" value={paddingY} onChange={handlePaddingYChange}/>
</p>
<p>
<input type="checkbox" checked={debug} onChange={(e) => {
setDebug(e.target.checked)
}} id="debug_mode" />
}} id="debug_mode"/>
<label htmlFor="debug_mode">Debug mode</label>

</p>
<DndProvider backend={TouchBackend} options={{enableMouseEvents: true}}>
<Draggable />
<Preview text="default" kind="default" />
<Preview text="with ref" kind="ref" />
<Draggable/>
<Preview text="default" kind="default"/>
<Preview text="with ref" kind="ref" placement={previewPlacement} padding={{x: Number(paddingX), y: Number(paddingY)}}/>
{debug ? (
<>
<Preview text="custom ClientOffset" kind="custom_client" />
<Preview text="custom SourceClientOffset" kind="custom_source_client" />
<Preview text="custom ClientOffset" kind="custom_client"/>
<Preview text="custom SourceClientOffset" kind="custom_source_client"/>
</>
) : null}
</DndProvider>
Expand Down
11 changes: 9 additions & 2 deletions packages/react-dnd-preview/src/Preview.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import React, {ReactNode} from 'react'
import {usePreview} from './usePreview'
import {Context, PreviewState} from './Context'
import {PreviewPlacement, Point} from './offsets'

export type PreviewGenerator<T = unknown, El extends Element = Element> = (state: PreviewState<T, El>) => JSX.Element

export type PreviewProps<T = unknown, El extends Element = Element> = {
export type PreviewProps<T = unknown, El extends Element = Element> = ({
children: PreviewGenerator<T, El> | ReactNode
} | {
generator: PreviewGenerator<T, El>,
}) & {
placement?: PreviewPlacement,
padding?: Point,
}

export const Preview = <T = unknown, El extends Element = Element>(props: PreviewProps<T, El>): JSX.Element | null => {
const result = usePreview<T, El>()
const result = usePreview<T, El>({
placement: props.placement,
padding: props.padding,
})

if (!result.display) {
return null
Expand Down
140 changes: 88 additions & 52 deletions packages/react-dnd-preview/src/__tests__/usePreview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {renderHook, act} from '@testing-library/react'
import {MockDragMonitor} from '@mocks/mocks'
import {__setMockMonitor} from '@mocks/react-dnd'
import {MutableRefObject} from 'react'
import { PreviewPlacement } from '../offsets'

describe('usePreview hook', () => {
beforeEach(() => {
Expand Down Expand Up @@ -138,57 +139,92 @@ describe('usePreview hook', () => {
})
})

test('return true and data when DnD is in progress (with ref and parent offset)', async () => {
__setMockMonitor({
...MockDragMonitor<{bluh: string}>({bluh: 'fake'}),
isDragging() {return true},
getItemType() {return 'no'},
getClientOffset() {return {x: 1, y: 2}},
getInitialClientOffset() {return {x: 1, y: 2}},
getInitialSourceClientOffset() {return {x: 0, y: 1}},
})
const {result, rerender} = renderHook(() => {return usePreview() as usePreviewStateFull})
const {current: {display, monitor: _monitor, ref, ...rest}} = result
expect(display).toBe(true)
expect(ref).not.toBeNull()
expect(rest).toEqual({
item: {bluh: 'fake'},
itemType: 'no',
style: {
pointerEvents: 'none',
position: 'fixed',
left: 0,
top: 0,
WebkitTransform: 'translate(0.0px, 1.0px)',
transform: 'translate(0.0px, 1.0px)',
},
})
await act<void>(() => {
// FIXME: not great...
(ref as MutableRefObject<HTMLDivElement>).current = {
...document.createElement('div'),
getBoundingClientRect() {
return {
width: 100, height: 70,
x: 0, y: 0, bottom: 0, left: 0, right: 0, top: 0,
toJSON() { },
}
const cases: { placement?: PreviewPlacement, expectedTransform: string }[] = [
{
expectedTransform: 'translate(-49.0px, -33.0px)',
}, {
placement: 'center',
expectedTransform: 'translate(-49.0px, -33.0px)',
}, {
placement: 'top',
expectedTransform: 'translate(-49.0px, 2.0px)',
}, {
placement: 'top-start',
expectedTransform: 'translate(1.0px, 2.0px)',
}, {
placement: 'top-end',
expectedTransform: 'translate(-99.0px, 2.0px)',
}, {
placement: 'bottom',
expectedTransform: 'translate(-49.0px, -68.0px)',
}, {
placement: 'bottom-start',
expectedTransform: 'translate(1.0px, -68.0px)',
}, {
placement: 'bottom-end',
expectedTransform: 'translate(-99.0px, -68.0px)',
}, {
placement: 'left',
expectedTransform: 'translate(1.0px, -33.0px)',
}, {
placement: 'right',
expectedTransform: 'translate(-99.0px, -33.0px)',
}]

test.each(cases)(
'return true and data when DnD is in progress (with ref, parent offset and placement $placement)',
async ({placement, expectedTransform}) => {
__setMockMonitor({
...MockDragMonitor<{bluh: string}>({bluh: 'fake'}),
isDragging() {return true},
getItemType() {return 'no'},
getClientOffset() {return {x: 1, y: 2}},
getInitialClientOffset() {return {x: 1, y: 2}},
getInitialSourceClientOffset() {return {x: 0, y: 1}},
})
const {result, rerender} = renderHook(() => {return usePreview({ placement }) as usePreviewStateFull})
const {current: {display, monitor: _monitor, ref, ...rest}} = result
expect(display).toBe(true)
expect(ref).not.toBeNull()
expect(rest).toEqual({
item: {bluh: 'fake'},
itemType: 'no',
style: {
pointerEvents: 'none',
position: 'fixed',
left: 0,
top: 0,
WebkitTransform: 'translate(0.0px, 1.0px)',
transform: 'translate(0.0px, 1.0px)',
},
}
})
rerender()
const {current: {display: _display, monitor: _monitor2, ref: _ref, ...rest2}} = result
expect(rest2).toEqual({
item: {bluh: 'fake'},
itemType: 'no',
style: {
pointerEvents: 'none',
position: 'fixed',
left: 0,
top: 0,
WebkitTransform: 'translate(-49.0px, -33.0px)',
transform: 'translate(-49.0px, -33.0px)',
},
})
})
})
await act<void>(() => {
// FIXME: not great...
(ref as MutableRefObject<HTMLDivElement>).current = {
...document.createElement('div'),
getBoundingClientRect() {
return {
width: 100, height: 70,
x: 0, y: 0, bottom: 0, left: 0, right: 0, top: 0,
toJSON() { },
}
},
}
})
rerender({ placement })
const {current: {display: _display, monitor: _monitor2, ref: _ref, ...rest2}} = result
expect(rest2).toEqual({
item: {bluh: 'fake'},
itemType: 'no',
style: {
pointerEvents: 'none',
position: 'fixed',
left: 0,
top: 0,
WebkitTransform: expectedTransform,
transform: expectedTransform,
},
})
}
)
})
1 change: 1 addition & 0 deletions packages/react-dnd-preview/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { Preview } from './Preview'
export type { PreviewProps, PreviewGenerator } from './Preview'
export { usePreview } from './usePreview'
export type { PreviewPlacement, Point } from './offsets'
export type { usePreviewState, usePreviewStateContent } from './usePreview'
export { Context } from './Context'
export type { PreviewState } from './Context'
60 changes: 57 additions & 3 deletions packages/react-dnd-preview/src/offsets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,31 @@ export type Point = {
y: number,
}

export type PreviewPlacement =
| 'center'
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'right'

const subtract = (a: Point, b: Point): Point => {
return {
x: a.x - b.x,
y: a.y - b.y,
}
}

const add = (a: Point, b: Point): Point => {
return {
x: a.x + b.x,
y: a.y + b.y,
}
}

const calculateParentOffset = (monitor: DragLayerMonitor): Point => {
const client = monitor.getInitialClientOffset()
const source = monitor.getInitialSourceClientOffset()
Expand All @@ -30,7 +48,42 @@ const calculateParentOffset = (monitor: DragLayerMonitor): Point => {
return subtract(client, source)
}

export const calculatePointerPosition = (monitor: DragLayerMonitor, childRef: RefObject<Element>): Point | null => {
const calculateXOffset = (placement: PreviewPlacement, bb: DOMRect): number => {
switch (placement) {
case 'left':
case 'top-start':
case 'bottom-start':
return 0
case 'right':
case 'top-end':
case 'bottom-end':
return bb.width
default:
return bb.width / 2
}
}

const calculateYOffset = (placement: PreviewPlacement, bb: DOMRect): number => {
switch (placement) {
case 'top':
case 'top-start':
case 'top-end':
return 0
case 'bottom':
case 'bottom-start':
case 'bottom-end':
return bb.height
default:
return bb.height / 2
}
}

export const calculatePointerPosition = (
monitor: DragLayerMonitor,
childRef: RefObject<Element>,
placement: PreviewPlacement = 'center',
padding: Point = { x: 0, y: 0 },
): Point | null => {
const offset = monitor.getClientOffset()
if (offset === null) {
return null
Expand All @@ -43,6 +96,7 @@ export const calculatePointerPosition = (monitor: DragLayerMonitor, childRef: Re
}

const bb = childRef.current.getBoundingClientRect()
const middle = {x: bb.width / 2, y: bb.height / 2}
return subtract(offset, middle)
const previewOffset = { x: calculateXOffset(placement, bb), y: calculateYOffset(placement, bb) }

return add(subtract(offset, previewOffset), padding)
}
Loading

0 comments on commit 2c8140a

Please sign in to comment.