Skip to content

Commit

Permalink
Drop zone provider: option to avoid wrapper element (#27079)
Browse files Browse the repository at this point in the history
* Drop zone provider: option to avoid wrapper element

* Fix old component

* Fix media placeholder drag over state
  • Loading branch information
ellatrix authored Dec 3, 2020
1 parent 0468fc8 commit b0170b0
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 122 deletions.
247 changes: 129 additions & 118 deletions packages/components/src/drop-zone/provider.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/**
* External dependencies
*/
import { find, some, filter, includes } from 'lodash';
import { find, some, filter, includes, throttle } from 'lodash';

/**
* WordPress dependencies
*/
import {
createContext,
useCallback,
useEffect,
useRef,
useContext,
} from '@wordpress/element';
import { useThrottle } from '@wordpress/compose';
import { getFilesFromDataTransfer } from '@wordpress/dom';
import isShallowEqual from '@wordpress/is-shallow-equal';

Expand All @@ -22,13 +21,16 @@ const { Provider } = Context;

function getDragEventType( { dataTransfer } ) {
if ( dataTransfer ) {
if ( getFilesFromDataTransfer( dataTransfer ).length > 0 ) {
return 'file';
}

// Use lodash `includes` here as in the Edge browser `types` is implemented
// as a DomStringList, whereas in other browsers it's an array. `includes`
// happily works with both types.
if (
includes( dataTransfer.types, 'Files' ) ||
getFilesFromDataTransfer( dataTransfer ).length > 0
) {
return 'file';
}

if ( includes( dataTransfer.types, 'text/html' ) ) {
return 'html';
}
Expand Down Expand Up @@ -74,7 +76,7 @@ function getPosition( event ) {

function getHoveredDropZone( dropZones, position, dragEventType ) {
const hoveredDropZones = filter(
Array.from( dropZones.current ),
Array.from( dropZones ),
( dropZone ) =>
isTypeSupportedByDropZone( dragEventType, dropZone ) &&
isWithinElementBounds(
Expand Down Expand Up @@ -107,126 +109,121 @@ export const INITIAL_DROP_ZONE_STATE = {
type: null,
};

export default function DropZoneProvider( { children } ) {
const ref = useRef();
const dropZones = useRef( new Set( [] ) );
const lastRelative = useRef();
export function useDrop( ref ) {
const dropZones = useContext( Context );

const updateDragZones = useCallback( ( event ) => {
if (
lastRelative.current &&
lastRelative.current.contains( event.target )
) {
return;
}
useEffect( () => {
const { ownerDocument } = ref.current;
const { defaultView } = ownerDocument;

const dragEventType = getDragEventType( event );
const position = getPosition( event );
const hoveredDropZone = getHoveredDropZone(
dropZones,
position,
dragEventType
);
let lastRelative;

if ( hoveredDropZone && hoveredDropZone.isRelative ) {
lastRelative.current = hoveredDropZone.element.current.offsetParent;
} else {
lastRelative.current = null;
}
function updateDragZones( event ) {
if ( lastRelative && lastRelative.contains( event.target ) ) {
return;
}

// Notifying the dropzones
dropZones.current.forEach( ( dropZone ) => {
const isDraggingOverDropZone = dropZone === hoveredDropZone;
const newState = {
isDraggingOverDocument: isTypeSupportedByDropZone(
dragEventType,
dropZone
),
isDraggingOverElement: isDraggingOverDropZone,
x:
isDraggingOverDropZone && dropZone.withPosition
? position.x
: null,
y:
isDraggingOverDropZone && dropZone.withPosition
? position.y
: null,
type: isDraggingOverDropZone ? dragEventType : null,
};

dropZone.setState( ( state ) => {
if ( isShallowEqual( state, newState ) ) {
return state;
}
const dragEventType = getDragEventType( event );
const position = getPosition( event );
const hoveredDropZone = getHoveredDropZone(
dropZones,
position,
dragEventType
);

if ( hoveredDropZone && hoveredDropZone.isRelative ) {
lastRelative = hoveredDropZone.element.current.offsetParent;
} else {
lastRelative = null;
}

return newState;
// Notifying the dropzones
dropZones.forEach( ( dropZone ) => {
const isDraggingOverDropZone = dropZone === hoveredDropZone;
const newState = {
isDraggingOverDocument: isTypeSupportedByDropZone(
dragEventType,
dropZone
),
isDraggingOverElement: isDraggingOverDropZone,
x:
isDraggingOverDropZone && dropZone.withPosition
? position.x
: null,
y:
isDraggingOverDropZone && dropZone.withPosition
? position.y
: null,
type: isDraggingOverDropZone ? dragEventType : null,
};

dropZone.setState( ( state ) => {
if ( isShallowEqual( state, newState ) ) {
return state;
}

return newState;
} );
} );
} );

event.preventDefault();
}, [] );
event.preventDefault();
}

const throttledUpdateDragZones = useThrottle( updateDragZones, 200 );
const throttledUpdateDragZones = throttle( updateDragZones, 200 );

const onDragOver = useCallback(
( event ) => {
function onDragOver( event ) {
throttledUpdateDragZones( event );
event.preventDefault();
},
[ throttledUpdateDragZones ]
);

const resetDragState = useCallback( () => {
// Avoid throttled drag over handler calls
throttledUpdateDragZones.cancel();
}

dropZones.current.forEach( ( dropZone ) =>
dropZone.setState( INITIAL_DROP_ZONE_STATE )
);
}, [] );

function onDrop( event ) {
// This seemingly useless line has been shown to resolve a Safari issue
// where files dragged directly from the dock are not recognized
event.dataTransfer && event.dataTransfer.files.length; // eslint-disable-line no-unused-expressions

const dragEventType = getDragEventType( event );
const position = getPosition( event );
const hoveredDropZone = getHoveredDropZone(
dropZones,
position,
dragEventType
);
function resetDragState() {
// Avoid throttled drag over handler calls
throttledUpdateDragZones.cancel();

resetDragState();

if ( hoveredDropZone ) {
switch ( dragEventType ) {
case 'file':
hoveredDropZone.onFilesDrop(
getFilesFromDataTransfer( event.dataTransfer ),
position
);
break;
case 'html':
hoveredDropZone.onHTMLDrop(
event.dataTransfer.getData( 'text/html' ),
position
);
break;
case 'default':
hoveredDropZone.onDrop( event, position );
}
dropZones.forEach( ( dropZone ) =>
dropZone.setState( INITIAL_DROP_ZONE_STATE )
);
}

event.stopPropagation();
event.preventDefault();
}
function onDrop( event ) {
// This seemingly useless line has been shown to resolve a Safari issue
// where files dragged directly from the dock are not recognized
event.dataTransfer && event.dataTransfer.files.length; // eslint-disable-line no-unused-expressions

const dragEventType = getDragEventType( event );
const position = getPosition( event );
const hoveredDropZone = getHoveredDropZone(
dropZones,
position,
dragEventType
);

resetDragState();

if ( hoveredDropZone ) {
switch ( dragEventType ) {
case 'file':
hoveredDropZone.onFilesDrop(
getFilesFromDataTransfer( event.dataTransfer ),
position
);
break;
case 'html':
hoveredDropZone.onHTMLDrop(
event.dataTransfer.getData( 'text/html' ),
position
);
break;
case 'default':
hoveredDropZone.onDrop( event, position );
}
}

useEffect( () => {
const { ownerDocument } = ref.current;
const { defaultView } = ownerDocument;
event.stopPropagation();
event.preventDefault();
}

defaultView.addEventListener( 'drop', onDrop );
defaultView.addEventListener( 'dragover', onDragOver );
defaultView.addEventListener( 'mouseup', resetDragState );
// Note that `dragend` doesn't fire consistently for file and HTML drag
Expand All @@ -235,19 +232,33 @@ export default function DropZoneProvider( { children } ) {
defaultView.addEventListener( 'dragend', resetDragState );

return () => {
defaultView.removeEventListener( 'drop', onDrop );
defaultView.removeEventListener( 'dragover', onDragOver );
defaultView.removeEventListener( 'mouseup', resetDragState );
defaultView.removeEventListener( 'dragend', resetDragState );
};
}, [ onDragOver, resetDragState ] );
}, [ ref, dropZones ] );
}

export function DropZoneContextProvider( props ) {
const ref = useRef( new Set( [] ) );
return <Provider { ...props } value={ ref.current } />;
}

function DropContainer( { children } ) {
const ref = useRef();
useDrop( ref );
return (
<div
ref={ ref }
onDrop={ onDrop }
className="components-drop-zone__provider"
>
<Provider value={ dropZones.current }>{ children }</Provider>
<div ref={ ref } className="components-drop-zone__provider">
{ children }
</div>
);
}

export default function DropZoneProvider( { children } ) {
return (
<DropZoneContextProvider>
<DropContainer>{ children }</DropContainer>
</DropZoneContextProvider>
);
}
6 changes: 5 additions & 1 deletion packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export {
default as DropZone,
useDropZone as __unstableUseDropZone,
} from './drop-zone';
export { default as DropZoneProvider } from './drop-zone/provider';
export {
default as DropZoneProvider,
DropZoneContextProvider as __unstableDropZoneContextProvider,
useDrop as __unstableUseDrop,
} from './drop-zone/provider';
export { default as Dropdown } from './dropdown';
export { default as DropdownMenu } from './dropdown-menu';
export { default as ExternalLink } from './external-link';
Expand Down
2 changes: 2 additions & 0 deletions packages/edit-post/src/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ScrollLock,
Popover,
FocusReturnProvider,
__unstableUseDrop as useDrop,
} from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import { PluginArea } from '@wordpress/plugins';
Expand Down Expand Up @@ -167,6 +168,7 @@ function Layout( { settings } ) {
);
const ref = useRef();

useDrop( ref );
useEditorStyles( ref, settings.styles );

return (
Expand Down
6 changes: 3 additions & 3 deletions packages/edit-post/src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { StrictMode, useMemo } from '@wordpress/element';
import {
KeyboardShortcuts,
SlotFillProvider,
DropZoneProvider,
__unstableDropZoneContextProvider as DropZoneContextProvider,
} from '@wordpress/components';

/**
Expand Down Expand Up @@ -160,7 +160,7 @@ function Editor( {
<StrictMode>
<EditPostSettings.Provider value={ settings }>
<SlotFillProvider>
<DropZoneProvider>
<DropZoneContextProvider>
<EditorProvider
settings={ editorSettings }
post={ post }
Expand All @@ -180,7 +180,7 @@ function Editor( {
</ErrorBoundary>
<PostLockedModal />
</EditorProvider>
</DropZoneProvider>
</DropZoneContextProvider>
</SlotFillProvider>
</EditPostSettings.Provider>
</StrictMode>
Expand Down

0 comments on commit b0170b0

Please sign in to comment.