Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
import { useCallback, useLayoutEffect, useRef, useState } from "react";
// Drag-and-drop for the zero-pin case: with no pins there's nothing to reorder,
// so this is just "drag one tile onto one target." A single placeholder marks
// the first pinnable slot; dropping on it pins the tile there. No slots, no
// geometry, no reorder preview — once there's a group, reordering is the classic
// useTopSitesDnD hook. Domain bits are injected, as elsewhere.
export function useZeroPinDrop({
baseSites,
isSponsored, // leading anchors the pin sits behind
onDragStart,
onReorder, // ({ site, title, fromIndex, toIndex }) — caller persists it
}) {
const [draggedIndex, setDraggedIndex] = useState(null);
const [draggedSite, setDraggedSite] = useState(null);
const [draggedTitle, setDraggedTitle] = useState(null);
const [over, setOver] = useState(false);
// Set on drop so the trailing dragend doesn't un-collapse the source before
// the pin commits (which swaps this container out entirely).
const droppedRef = useRef(false);
const placeholderElRef = useRef(null);
const setZeroPinRef = useCallback(el => {
placeholderElRef.current = el;
}, []);
// First pinnable slot (after any leading sponsored); the backend lands it at
// group position 0.
const slot = baseSites.findIndex(site => site && !isSponsored(site));
const insertIndex = slot === -1 ? 0 : slot;
const isOver = useCallback(event => {
const rect = placeholderElRef.current?.getBoundingClientRect();
return (
!!rect &&
event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom
);
}, []);
const resetDrag = useCallback(() => {
setDraggedIndex(null);
setDraggedSite(null);
setDraggedTitle(null);
setOver(false);
}, []);
const onDragEvent = useCallback(
(event, index, link, title) => {
switch (event.type) {
case "dragstart":
droppedRef.current = false;
setDraggedIndex(index);
setDraggedSite(link);
setDraggedTitle(title);
onDragStart?.(index);
break;
case "dragend":
if (!droppedRef.current) {
resetDrag();
} else {
// Keep the source collapsed until the pin commits and this unmounts.
setOver(false);
}
break;
}
},
[onDragStart, resetDrag]
);
const onDragOver = useCallback(
event => {
if (!draggedSite) {
return;
}
const nowOver = isOver(event);
if (nowOver) {
event.preventDefault();
}
if (over !== nowOver) {
setOver(nowOver);
}
},
[draggedSite, isOver, over]
);
const onDrop = useCallback(
event => {
if (!draggedSite || !isOver(event)) {
return;
}
event.preventDefault();
droppedRef.current = true;
onReorder?.({
site: draggedSite,
title: draggedTitle,
fromIndex: draggedIndex,
toIndex: insertIndex,
});
},
[draggedSite, draggedTitle, draggedIndex, isOver, onReorder, insertIndex]
);
const onDragLeave = useCallback(
event => {
if (event.currentTarget.contains(event.relatedTarget)) {
return;
}
if (over) {
setOver(false);
}
},
[over]
);
// The container no longer unmounts on the first pin, so clear our own drag
// state once the drop commits (new baseSites arrive after the refresh). Layout
// effect so the source doesn't un-collapse for a frame before the pin lands.
const prevBaseRef = useRef(baseSites);
useLayoutEffect(() => {
if (droppedRef.current && prevBaseRef.current !== baseSites) {
droppedRef.current = false;
resetDrag();
}
prevBaseRef.current = baseSites;
}, [baseSites, resetDrag]);
// Collapse the dragged source out of flow; the placeholder stands in for it
// (net-zero cells, so a full row won't overflow).
const sites = draggedSite
? baseSites.map(site =>
site && site.url === draggedSite.url
? { ...site, isCollapsed: true }
: site
)
: baseSites;
return {
sites,
draggedSite,
onDragEvent,
listProps: { onDragOver, onDrop, onDragLeave },
decorations: {
// Only open the drop placeholder while actually dragging; otherwise it
// would sit in the grid as an invisible (visibility:hidden) blank slot.
zeroPinSlot: draggedSite ? insertIndex : -1,
overZeroPin: over,
setZeroPinRef,
},
};
}