Skip to content

Commit

Permalink
HTML5Backend Event Swallowing Updates (#3052)
Browse files Browse the repository at this point in the history
* refactor: remove preventDefaulth in handleTopDragEnter

* refactor: remove preventDefault from handleSelectStart

* feat: add HTML5Backend options to unblock drag

* chore: cut semver doc

* fix: typo

* feat: add rootElement option for attaching event listeners to

* fix: revent some cuts

* fix: revert some more cuts

* fix: revert more cuts

* feat: use localized eventing option in DnDProvider on docsite

* fix: revert unblockNativeTypeEvents config options experiment

* fix: guard endDrag() invocations with isDragging()

* fix: error in dropTarget accept arrays

* chore: update semver impact

* chore: update semver
  • Loading branch information
darthtrevino committed Feb 22, 2021
1 parent 253a725 commit 339dd7a
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 54 deletions.
9 changes: 9 additions & 0 deletions .yarn/versions/9c51b640.yml
@@ -0,0 +1,9 @@
releases:
react-dnd: patch
react-dnd-html5-backend: minor

declined:
- react-dnd-documentation
- react-dnd-examples-decorators
- react-dnd-examples-hooks
- react-dnd-test-utils
62 changes: 32 additions & 30 deletions packages/backend-html5/src/HTML5BackendImpl.ts
Expand Up @@ -21,13 +21,9 @@ import {
import * as NativeTypes from './NativeTypes'
import { NativeDragSource } from './NativeDragSources/NativeDragSource'
import { OptionsReader } from './OptionsReader'
import { HTML5BackendContext } from './types'
import { HTML5BackendContext, HTML5BackendOptions } from './types'

declare global {
interface Window {
__isReactDndBackendSetUp: boolean | undefined
}
}
type RootNode = Node & { __isReactDndBackendSetUp: boolean | undefined }

export class HTML5BackendImpl implements Backend {
private options: OptionsReader
Expand Down Expand Up @@ -59,8 +55,9 @@ export class HTML5BackendImpl implements Backend {
public constructor(
manager: DragDropManager,
globalContext?: HTML5BackendContext,
options?: HTML5BackendOptions,
) {
this.options = new OptionsReader(globalContext)
this.options = new OptionsReader(globalContext, options)
this.actions = manager.getActions()
this.monitor = manager.getMonitor()
this.registry = manager.getRegistry()
Expand Down Expand Up @@ -90,29 +87,37 @@ export class HTML5BackendImpl implements Backend {
public get document(): Document | undefined {
return this.options.document
}
/**
* Get the root element to use for event subscriptions
*/
private get rootElement(): Node | undefined {
return this.options.rootElement as Node
}

public setup(): void {
if (this.window === undefined) {
const root = this.rootElement as RootNode | undefined
if (root === undefined) {
return
}

if (this.window.__isReactDndBackendSetUp) {
if (root.__isReactDndBackendSetUp) {
throw new Error('Cannot have two HTML5 backends at the same time.')
}
this.window.__isReactDndBackendSetUp = true
this.addEventListeners(this.window as Element)
root.__isReactDndBackendSetUp = true
this.addEventListeners(root)
}

public teardown(): void {
if (this.window === undefined) {
const root = this.rootElement as RootNode
if (root === undefined) {
return
}

this.window.__isReactDndBackendSetUp = false
this.removeEventListeners(this.window as Element)
root.__isReactDndBackendSetUp = false
this.removeEventListeners(this.rootElement as Element)
this.clearCurrentDragSourceNode()
if (this.asyncEndDragFrameId) {
this.window.cancelAnimationFrame(this.asyncEndDragFrameId)
this.window?.cancelAnimationFrame(this.asyncEndDragFrameId)
}
}

Expand Down Expand Up @@ -324,11 +329,11 @@ export class HTML5BackendImpl implements Backend {

private endDragIfSourceWasRemovedFromDOM = (): void => {
const node = this.currentDragSourceNode
if (this.isNodeInDocument(node)) {
if (node == null || this.isNodeInDocument(node)) {
return
}

if (this.clearCurrentDragSourceNode()) {
if (this.clearCurrentDragSourceNode() && this.monitor.isDragging()) {
this.actions.endDrag()
}
}
Expand Down Expand Up @@ -356,13 +361,10 @@ export class HTML5BackendImpl implements Backend {
// * https://github.com/react-dnd/react-dnd/issues/869
//
this.mouseMoveTimeoutTimer = (setTimeout(() => {
return (
this.window &&
this.window.addEventListener(
'mousemove',
this.endDragIfSourceWasRemovedFromDOM,
true,
)
return this.rootElement?.addEventListener(
'mousemove',
this.endDragIfSourceWasRemovedFromDOM,
true,
)
}, MOUSE_MOVE_TIMEOUT) as any) as number
}
Expand All @@ -371,9 +373,9 @@ export class HTML5BackendImpl implements Backend {
if (this.currentDragSourceNode) {
this.currentDragSourceNode = null

if (this.window) {
this.window.clearTimeout(this.mouseMoveTimeoutTimer || undefined)
this.window.removeEventListener(
if (this.rootElement) {
this.window?.clearTimeout(this.mouseMoveTimeoutTimer || undefined)
this.rootElement.removeEventListener(
'mousemove',
this.endDragIfSourceWasRemovedFromDOM,
true,
Expand Down Expand Up @@ -515,7 +517,7 @@ export class HTML5BackendImpl implements Backend {
}

public handleTopDragEndCapture = (): void => {
if (this.clearCurrentDragSourceNode()) {
if (this.clearCurrentDragSourceNode() && this.monitor.isDragging()) {
// Firefox can dispatch this event in an infinite loop
// if dragend handler does something like showing an alert.
// Only proceed if we have not handled it already.
Expand Down Expand Up @@ -641,7 +643,7 @@ export class HTML5BackendImpl implements Backend {
}

if (this.isDraggingNativeItem()) {
this.endDragNativeItem()
setTimeout(() => this.endDragNativeItem(), 0)
}
}

Expand Down Expand Up @@ -671,7 +673,7 @@ export class HTML5BackendImpl implements Backend {

if (this.isDraggingNativeItem()) {
this.endDragNativeItem()
} else {
} else if (this.monitor.isDragging()) {
this.actions.endDrag()
}
}
Expand Down
13 changes: 11 additions & 2 deletions packages/backend-html5/src/OptionsReader.ts
@@ -1,11 +1,16 @@
import { HTML5BackendContext } from './types'
import { HTML5BackendContext, HTML5BackendOptions } from './types'

export class OptionsReader {
public ownerDocument: Document | null = null
private globalContext: HTML5BackendContext
private optionsArgs?: HTML5BackendOptions

public constructor(globalContext: HTML5BackendContext) {
public constructor(
globalContext: HTML5BackendContext,
options?: HTML5BackendOptions,
) {
this.globalContext = globalContext
this.optionsArgs = options
}

public get window(): Window | undefined {
Expand All @@ -26,4 +31,8 @@ export class OptionsReader {
return undefined
}
}

public get rootElement(): Node | undefined {
return this.optionsArgs?.rootElement || this.window
}
}
5 changes: 3 additions & 2 deletions packages/backend-html5/src/index.ts
@@ -1,13 +1,14 @@
import { HTML5BackendImpl } from './HTML5BackendImpl'
import * as NativeTypes from './NativeTypes'
import { DragDropManager, BackendFactory } from 'dnd-core'
import { HTML5BackendContext } from './types'
import { HTML5BackendContext, HTML5BackendOptions } from './types'
export { getEmptyImage } from './getEmptyImage'
export { NativeTypes }

export const HTML5Backend: BackendFactory = function createBackend(
manager: DragDropManager,
context?: HTML5BackendContext,
options?: HTML5BackendOptions,
): HTML5BackendImpl {
return new HTML5BackendImpl(manager, context)
return new HTML5BackendImpl(manager, context, options)
}
10 changes: 10 additions & 0 deletions packages/backend-html5/src/types.ts
@@ -1 +1,11 @@
export type HTML5BackendContext = Window | undefined

/**
* Configuration options for the HTML5Backend
*/
export interface HTML5BackendOptions {
/**
* The root DOM node to use for subscribing to events. Default=Window
*/
rootElement: Node
}
46 changes: 27 additions & 19 deletions packages/docsite/src/components/layout.tsx
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */
declare const require: any

import { FC, memo } from 'react'
import { FC, memo, useMemo, useCallback, useState } from 'react'
import { Helmet } from 'react-helmet'
import styled from 'styled-components'
import { HTML5Backend } from 'react-dnd-html5-backend'
Expand Down Expand Up @@ -43,6 +43,9 @@ export const Layout: FC<LayoutProps> = memo(function Layout(props) {
const hideSidebar = props.hideSidebar || sitepath === '/about'
const debugMode = isDebugMode()
const touchBackend = isTouchBackend()
const [dndArea, setDndArea] = useState<HTMLDivElement | null>(null)
const html5Options = useMemo(() => ({ rootElement: dndArea }), [dndArea])

return (
<>
<Helmet title="React DnD" meta={HEADER_META} link={HEADER_LINK}>
Expand All @@ -53,25 +56,30 @@ export const Layout: FC<LayoutProps> = memo(function Layout(props) {
/>
</Helmet>
<Header debugMode={debugMode} touchBackend={touchBackend} />
<DndProvider
backend={touchBackend ? TouchBackend : HTML5Backend}
options={touchBackend ? touchBackendOptions : undefined}
debugMode={debugMode}
>
<ContentContainer>
<PageBody hasSidebar={sitepath !== '/about'}>
{hideSidebar ? null : (
<SidebarContainer>
<Sidebar
groups={sidebarItems}
location={location ? location.pathname : '/'}
/>
</SidebarContainer>

<ContentContainer>
<PageBody hasSidebar={sitepath !== '/about'}>
{hideSidebar ? null : (
<SidebarContainer>
<Sidebar
groups={sidebarItems}
location={location ? location.pathname : '/'}
/>
</SidebarContainer>
)}
<ChildrenContainer ref={useCallback((node) => setDndArea(node), [])}>
{dndArea == null ? null : (
<DndProvider
backend={touchBackend ? TouchBackend : HTML5Backend}
options={touchBackend ? touchBackendOptions : html5Options}
debugMode={debugMode}
>
{children}
</DndProvider>
)}
<ChildrenContainer>{children}</ChildrenContainer>
</PageBody>
</ContentContainer>
</DndProvider>
</ChildrenContainer>
</PageBody>
</ContentContainer>
</>
)
})
Expand Down
10 changes: 9 additions & 1 deletion packages/react-dnd/src/hooks/useDrop.ts
Expand Up @@ -89,6 +89,14 @@ function useDropHandler<
} as DropTarget
}, [monitor])

/**
* If we pass in spec.current.accept directly, the React hook will
* re-fire when the array-instance changes. Instead, we pass the
* accept items directly into the deps if it's an array
*/
const specDeps = Array.isArray(spec.current?.accept)
? spec.current?.accept
: [spec.current.accept]
useIsomorphicLayoutEffect(
function registerHandler() {
const [handlerId, unregister] = registerTarget(
Expand All @@ -100,6 +108,6 @@ function useDropHandler<
connector.receiveHandlerId(handlerId)
return unregister
},
[monitor, connector, spec.current.accept],
[monitor, connector, ...specDeps],
)
}

0 comments on commit 339dd7a

Please sign in to comment.