diff --git a/src/state/droppable/get-droppable.ts b/src/state/droppable/get-droppable.ts index 1c72f93a2..8847ba271 100644 --- a/src/state/droppable/get-droppable.ts +++ b/src/state/droppable/get-droppable.ts @@ -32,6 +32,7 @@ interface Args { page: BoxModel; closest?: Closest | null; transform: Transform | null; + parents: DroppableDescriptor[]; } export default ({ @@ -44,6 +45,7 @@ export default ({ page, closest, transform, + parents, }: Args): DroppableDimension => { const frame: Scrollable | null = (() => { if (!closest) { @@ -98,6 +100,7 @@ export default ({ frame, subject, transform, + parents, }; return dimension; diff --git a/src/state/get-droppable-over.ts b/src/state/get-droppable-over.ts index bfc9532ab..42a6eacca 100644 --- a/src/state/get-droppable-over.ts +++ b/src/state/get-droppable-over.ts @@ -75,6 +75,53 @@ function getFurthestAway({ return sorted[0] ? sorted[0].id : null; } +/** + * normalizeFamilies + * + * Groups all items that share a common root `parent`, and selects the deepest item + * in that group that contains the center point of the dragged item to represent + * the "family". + */ +function normalizeFamilies( + pageBorderBox: Rect, + candidates: DroppableDimension[], +) { + const families = candidates.reduce>( + (acc, candidate) => { + const familyName = candidate.parents[0]?.id || candidate.descriptor.id; + const family = acc[familyName] || []; + + const generation = candidate.parents.length; + + family[generation] = [...(family[generation] || []), candidate]; + + return { + ...acc, + [familyName]: family, + }; + }, + {}, + ); + + return Object.keys(families).map((familyName) => { + const family = families[familyName].flat(); + + const reversedFamily = [...family].reverse(); + + // Get first member of family that contains the draggable + const chosenMember = reversedFamily.find((member) => { + return ( + pageBorderBox.center.x < member.page.borderBox.right && + pageBorderBox.center.x > member.page.borderBox.left && + pageBorderBox.center.y > member.page.borderBox.top && + pageBorderBox.center.y < member.page.borderBox.bottom + ); + }); + + return chosenMember || family[0]; + }); +} + export default function getDroppableOver({ pageBorderBox, draggable, @@ -146,12 +193,19 @@ export default function getDroppableOver({ return candidates[0].descriptor.id; } - // Multiple options returned + // Select the best candidate from each group that share a common root ancestor + const normalizedCandidates = normalizeFamilies(pageBorderBox, candidates); + + // All candidates were in the same family + if (normalizedCandidates.length === 1) { + return normalizedCandidates[0].descriptor.id; + } + // Should only occur with really large items // Going to use fallback: distance from home return getFurthestAway({ pageBorderBox, draggable, - candidates, + candidates: normalizedCandidates, }); } diff --git a/src/types.ts b/src/types.ts index bfbb1d8a8..2028c563f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -154,6 +154,7 @@ export interface DroppableDimension { // what is visible through the frame subject: DroppableSubject; transform: Transform | null; + parents: DroppableDescriptor[]; } export interface DraggableLocation { droppableId: DroppableId; diff --git a/src/view/use-droppable-publisher/get-dimension.ts b/src/view/use-droppable-publisher/get-dimension.ts index 26d3449c0..09c0c59da 100644 --- a/src/view/use-droppable-publisher/get-dimension.ts +++ b/src/view/use-droppable-publisher/get-dimension.ts @@ -14,6 +14,7 @@ import getIframeOffset from '../iframe/get-iframe-offset'; import { applyOffsetBox } from '../iframe/apply-offset'; import { Transform, applyTransformBox, getTransform } from '../transform'; import { Offset } from '../iframe/offset-types'; +import { prefix } from '../data-attributes'; const getClient = ( targetRef: HTMLElement, @@ -96,6 +97,37 @@ interface Args { transform: Transform | null; } +const getParents = (ref: HTMLElement) => { + const contextId = ref.getAttribute(`${prefix}-droppable-context-id`); + + const parentDescriptors: DroppableDescriptor[] = []; + + if (!contextId) return []; + + let currentEl: HTMLElement | null | undefined = ref; + + while (currentEl) { + currentEl = currentEl.parentElement?.closest( + `[${prefix}-droppable-context-id="${contextId}"]`, + ); + + const id = currentEl?.getAttribute(`${prefix}-droppable-id`); + + if (id) { + parentDescriptors.push({ + id, + mode: 'standard', + type: 'DEFAULT', + }); + } + } + + // Parents need reversing + parentDescriptors.reverse(); + + return parentDescriptors; +}; + export default ({ ref, descriptor, @@ -132,6 +164,8 @@ export default ({ }; })(); + const parents = getParents(ref); + const dimension: DroppableDimension = getDroppableDimension({ descriptor, isEnabled: !isDropDisabled, @@ -142,6 +176,7 @@ export default ({ page, closest, transform, + parents, }); return dimension;