Skip to content

Commit e531a89

Browse files
committed
feat: select deepest candidate when using nested droppables
1 parent 9296074 commit e531a89

File tree

4 files changed

+106
-2
lines changed

4 files changed

+106
-2
lines changed

src/state/droppable/get-droppable.ts

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ interface Args {
3232
page: BoxModel;
3333
closest?: Closest | null;
3434
transform: Transform | null;
35+
parents: DroppableDescriptor[];
3536
}
3637

3738
export default ({
@@ -44,6 +45,7 @@ export default ({
4445
page,
4546
closest,
4647
transform,
48+
parents,
4749
}: Args): DroppableDimension => {
4850
const frame: Scrollable | null = (() => {
4951
if (!closest) {
@@ -98,6 +100,7 @@ export default ({
98100
frame,
99101
subject,
100102
transform,
103+
parents,
101104
};
102105

103106
return dimension;

src/state/get-droppable-over.ts

+70-2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,67 @@ function getFurthestAway({
7575
return sorted[0] ? sorted[0].id : null;
7676
}
7777

78+
/**
79+
* normalizeFamilies
80+
*
81+
* Groups all items that share a common root `parent`, and selects the deepest item
82+
* in that group that contains the center point of the dragged item to represent
83+
* the "family".
84+
*/
85+
function normalizeFamilies(
86+
pageBorderBox: Rect,
87+
candidates: DroppableDimension[],
88+
) {
89+
const families = candidates.reduce<Record<string, DroppableDimension[]>>(
90+
(acc, candidate) => {
91+
const familyName = candidate.parents[0]?.id || candidate.descriptor.id;
92+
const family = acc[familyName] || [];
93+
94+
const generation = candidate.parents.length;
95+
96+
let chosenCandidateId: string | null = candidate.descriptor.id;
97+
98+
if (family[generation]) {
99+
// Overlapping with 2 items at the same generation. get furthest away.
100+
101+
chosenCandidateId = getFurthestAway({
102+
pageBorderBox,
103+
draggable,
104+
candidates: [candidate, family[generation]],
105+
});
106+
}
107+
108+
if (chosenCandidateId) {
109+
family[generation] = candidate;
110+
}
111+
112+
return {
113+
...acc,
114+
[familyName]: family,
115+
};
116+
},
117+
{},
118+
);
119+
120+
return Object.keys(families).map((familyName) => {
121+
const family = families[familyName];
122+
123+
const reversedFamily = [...family].reverse();
124+
125+
// Get first member of family that contains the draggable
126+
const chosenMember = reversedFamily.find((member) => {
127+
return (
128+
pageBorderBox.center.x < member.page.borderBox.right &&
129+
pageBorderBox.center.x > member.page.borderBox.left &&
130+
pageBorderBox.center.y > member.page.borderBox.top &&
131+
pageBorderBox.center.y < member.page.borderBox.bottom
132+
);
133+
});
134+
135+
return chosenMember || family[0];
136+
});
137+
}
138+
78139
export default function getDroppableOver({
79140
pageBorderBox,
80141
draggable,
@@ -146,12 +207,19 @@ export default function getDroppableOver({
146207
return candidates[0].descriptor.id;
147208
}
148209

149-
// Multiple options returned
210+
// Select the best candidate from each group that share a common root ancestor
211+
const normalizedCandidates = normalizeFamilies(pageBorderBox, candidates);
212+
213+
// All candidates were in the same family
214+
if (normalizedCandidates.length === 1) {
215+
return normalizedCandidates[0].descriptor.id;
216+
}
217+
150218
// Should only occur with really large items
151219
// Going to use fallback: distance from home
152220
return getFurthestAway({
153221
pageBorderBox,
154222
draggable,
155-
candidates,
223+
candidates: normalizedCandidates,
156224
});
157225
}

src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export interface DroppableDimension {
154154
// what is visible through the frame
155155
subject: DroppableSubject;
156156
transform: Transform | null;
157+
parents: DroppableDescriptor[];
157158
}
158159
export interface DraggableLocation {
159160
droppableId: DroppableId;

src/view/use-droppable-publisher/get-dimension.ts

+32
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import getIframeOffset from '../iframe/get-iframe-offset';
1414
import { applyOffsetBox } from '../iframe/apply-offset';
1515
import { Transform, applyTransformBox, getTransform } from '../transform';
1616
import { Offset } from '../iframe/offset-types';
17+
import { prefix } from '../data-attributes';
1718

1819
const getClient = (
1920
targetRef: HTMLElement,
@@ -96,6 +97,34 @@ interface Args {
9697
transform: Transform | null;
9798
}
9899

100+
const getParents = (ref: HTMLElement) => {
101+
const contextId = ref.getAttribute(`${prefix}-droppable-context-id`);
102+
103+
const parentDescriptors: DroppableDescriptor[] = [];
104+
105+
if (!contextId) return [];
106+
107+
let currentEl: HTMLElement | null | undefined = ref;
108+
109+
while (currentEl) {
110+
currentEl = currentEl.parentElement?.closest(
111+
`[${prefix}-droppable-context-id="${contextId}"]`,
112+
);
113+
114+
const id = currentEl?.getAttribute(`${prefix}-droppable-id`);
115+
116+
if (id) {
117+
parentDescriptors.push({
118+
id,
119+
mode: 'standard',
120+
type: 'DEFAULT',
121+
});
122+
}
123+
}
124+
125+
return parentDescriptors;
126+
};
127+
99128
export default ({
100129
ref,
101130
descriptor,
@@ -132,6 +161,8 @@ export default ({
132161
};
133162
})();
134163

164+
const parents = getParents(ref);
165+
135166
const dimension: DroppableDimension = getDroppableDimension({
136167
descriptor,
137168
isEnabled: !isDropDisabled,
@@ -142,6 +173,7 @@ export default ({
142173
page,
143174
closest,
144175
transform,
176+
parents,
145177
});
146178

147179
return dimension;

0 commit comments

Comments
 (0)