@hello-pangea/dnd
Version:
Beautiful and accessible drag and drop for lists with React
310 lines (271 loc) • 9.08 kB
text/typescript
import { connect } from 'react-redux';
import memoizeOne from 'memoize-one';
import { FunctionComponent } from 'react';
import { invariant } from '../../invariant';
import type {
State,
DroppableId,
DraggableId,
CompletedDrag,
DraggableDimension,
DimensionMap,
TypeId,
Critical,
DraggableRubric,
DraggableDescriptor,
} from '../../types';
import type {
MapProps,
InternalOwnProps,
DroppableProps,
DefaultProps,
Selector,
DispatchProps,
DroppableStateSnapshot,
UseClone,
DraggableChildrenFn,
} from './droppable-types';
import Droppable from './droppable';
import isStrictEqual from '../is-strict-equal';
import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over';
import { updateViewportMaxScroll as updateViewportMaxScrollAction } from '../../state/action-creators';
import isDragging from '../../state/is-dragging';
import StoreContext from '../context/store-context';
import whatIsDraggedOverFromResult from '../../state/droppable/what-is-dragged-over-from-result';
function getBody(): HTMLElement {
invariant(document.body, 'document.body is not ready');
return document.body;
}
const defaultProps: DefaultProps = {
mode: 'standard',
type: 'DEFAULT',
direction: 'vertical',
isDropDisabled: false,
isCombineEnabled: false,
ignoreContainerClipping: false,
renderClone: null,
getContainerForClone: getBody,
};
const attachDefaultPropsToOwnProps = (ownProps: InternalOwnProps) => {
// We need to assign default props manually because upcoming React version will stop supporting
// defaultProps on functional components.
// see: https://github.com/facebook/react/pull/25699
let mergedProps = { ...ownProps };
let defaultPropKey: keyof typeof defaultProps;
for (defaultPropKey in defaultProps) {
if (ownProps[defaultPropKey] === undefined) {
mergedProps = {
...mergedProps,
[defaultPropKey]: defaultProps[defaultPropKey],
};
}
}
return mergedProps;
};
const isMatchingType = (type: TypeId, critical: Critical): boolean =>
type === critical.droppable.type;
const getDraggable = (
critical: Critical,
dimensions: DimensionMap,
): DraggableDimension => dimensions.draggables[critical.draggable.id];
// Returning a function to ensure each
// Droppable gets its own selector
export const makeMapStateToProps = (): Selector => {
const idleWithAnimation: MapProps = {
placeholder: null,
shouldAnimatePlaceholder: true,
snapshot: {
isDraggingOver: false,
draggingOverWith: null,
draggingFromThisWith: null,
isUsingPlaceholder: false,
},
useClone: null,
};
const idleWithoutAnimation = {
...idleWithAnimation,
shouldAnimatePlaceholder: false,
};
const getDraggableRubric = memoizeOne(
(descriptor: DraggableDescriptor): DraggableRubric => ({
draggableId: descriptor.id,
type: descriptor.type,
source: {
index: descriptor.index,
droppableId: descriptor.droppableId,
},
}),
);
const getMapProps = memoizeOne(
(
id: DroppableId,
isEnabled: boolean,
isDraggingOverForConsumer: boolean,
isDraggingOverForImpact: boolean,
dragging: DraggableDimension,
// snapshot: StateSnapshot,
renderClone?: DraggableChildrenFn | null,
): MapProps => {
const draggableId: DraggableId = dragging.descriptor.id;
const isHome: boolean = dragging.descriptor.droppableId === id;
if (isHome) {
const useClone: UseClone | null = renderClone
? {
render: renderClone,
dragging: getDraggableRubric(dragging.descriptor),
}
: null;
const snapshot: DroppableStateSnapshot = {
isDraggingOver: isDraggingOverForConsumer,
draggingOverWith: isDraggingOverForConsumer ? draggableId : null,
draggingFromThisWith: draggableId,
isUsingPlaceholder: true,
};
return {
placeholder: dragging.placeholder,
shouldAnimatePlaceholder: false,
snapshot,
useClone,
};
}
if (!isEnabled) {
return idleWithoutAnimation;
}
// not over foreign list - return idle
if (!isDraggingOverForImpact) {
return idleWithAnimation;
}
const snapshot: DroppableStateSnapshot = {
isDraggingOver: isDraggingOverForConsumer,
draggingOverWith: draggableId,
draggingFromThisWith: null,
isUsingPlaceholder: true,
};
return {
placeholder: dragging.placeholder,
// Animating placeholder in foreign list
shouldAnimatePlaceholder: true,
snapshot,
useClone: null,
};
},
);
const selector = (state: State, ownProps: InternalOwnProps): MapProps => {
// not checking if item is disabled as we need the home list to display a placeholder
const ownPropsWithDefaultProps = attachDefaultPropsToOwnProps(ownProps);
const id: DroppableId = ownPropsWithDefaultProps.droppableId;
const type: TypeId = ownPropsWithDefaultProps.type;
const isEnabled = !ownPropsWithDefaultProps.isDropDisabled;
const renderClone: DraggableChildrenFn | null =
ownPropsWithDefaultProps.renderClone;
if (isDragging(state)) {
const critical: Critical = state.critical;
if (!isMatchingType(type, critical)) {
return idleWithoutAnimation;
}
const dragging: DraggableDimension = getDraggable(
critical,
state.dimensions,
);
const isDraggingOver: boolean = whatIsDraggedOver(state.impact) === id;
return getMapProps(
id,
isEnabled,
isDraggingOver,
isDraggingOver,
dragging,
renderClone,
);
}
if (state.phase === 'DROP_ANIMATING') {
const completed: CompletedDrag = state.completed;
if (!isMatchingType(type, completed.critical)) {
return idleWithoutAnimation;
}
const dragging: DraggableDimension = getDraggable(
completed.critical,
state.dimensions,
);
// Snapshot based on result and not impact
// The result might be null (cancel) but the impact is populated
// to move everything back
return getMapProps(
id,
isEnabled,
whatIsDraggedOverFromResult(completed.result) === id,
whatIsDraggedOver(completed.impact) === id,
dragging,
renderClone,
);
}
if (state.phase === 'IDLE' && state.completed && !state.shouldFlush) {
const completed: CompletedDrag = state.completed;
if (!isMatchingType(type, completed.critical)) {
return idleWithoutAnimation;
}
// Looking at impact as this controls the placeholder
const wasOver: boolean = whatIsDraggedOver(completed.impact) === id;
const wasCombining = Boolean(
completed.impact.at && completed.impact.at.type === 'COMBINE',
);
const isHome: boolean = completed.critical.droppable.id === id;
if (wasOver) {
// if reordering we need to cut an animation immediately
// if merging: animate placeholder closed after drop
return wasCombining ? idleWithAnimation : idleWithoutAnimation;
}
// we need to animate the home placeholder closed if it is not
// being dropped into
if (isHome) {
return idleWithAnimation;
}
return idleWithoutAnimation;
}
// default: including when flushed
return idleWithoutAnimation;
};
return selector;
};
const mapDispatchToProps: DispatchProps = {
updateViewportMaxScroll: updateViewportMaxScrollAction,
};
// Abstract class allows to specify props and defaults to component.
// All other ways give any or do not let add default props.
// eslint-disable-next-line
/*::
class DroppableType extends Component<OwnProps> {
static defaultProps = defaultProps;
}
*/
// Leaning heavily on the default shallow equality checking
// that `connect` provides.
// It avoids needing to do it own within `Droppable`
const ConnectedDroppable = connect(
// returning a function so each component can do its own memoization
makeMapStateToProps,
// no dispatch props for droppable
mapDispatchToProps,
// We need to assign default props manually because upcoming React version will stop supporting
// defaultProps on functional components.
// see: https://github.com/facebook/react/pull/25699
(
stateProps: MapProps,
dispatchProps: DispatchProps,
ownProps: InternalOwnProps,
) => {
return {
...attachDefaultPropsToOwnProps(ownProps),
...stateProps,
...dispatchProps,
};
},
{
// Ensuring our context does not clash with consumers
context: StoreContext as any,
// Default value: shallowEqual
// Switching to a strictEqual as we return a memoized object on changes
areStatePropsEqual: isStrictEqual,
},
// FIXME: Typings are really complexe
)(Droppable) as unknown as FunctionComponent<DroppableProps>;
export default ConnectedDroppable;