Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/components/CellRendererComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,26 @@ function CellRendererComponent<T>(props: Props<T>) {

size.value = cellSize;
offset.value = cellOffset;
// Only release the held translate (see animStyle) once this cell's real,
// settled position is actually known — not merely once onCellLayout was
// called. onCellLayout is now also invoked speculatively, a frame after
// every index change (see the effect below), to force a re-measure for
// cells whose native onLayout doesn't refire on an index-only shift; that
// speculative call can land before the FlatList's own relayout has
// actually settled the cell into its new flow position. Clearing
// heldTanslate right there (rather than here) let the cell's transform
// drop to 0 a beat before its real position caught up, flashing/snapping
// visibly right as a reorder finished.
heldTanslate.value = 0;
};

const onFail = () => {
if (propsRef.current?.debug) {
console.log(`## on measure fail, index: ${index}`);
}
// Still release the hold on failure so a cell can't get stuck visibly
// offset forever just because one measurement attempt didn't land.
heldTanslate.value = 0;
};

const containerNode = containerRef.current;
Expand All @@ -99,18 +113,19 @@ function CellRendererComponent<T>(props: Props<T>) {
});

const onCellLayout = useStableCallback((e?: LayoutChangeEvent) => {
heldTanslate.value = 0;
updateCellMeasurements();
if (onLayout && e) onLayout(e);
});

useEffect(() => {
if (isWeb) {
// onLayout isn't called on web when the cell index changes, so we manually re-measure
requestAnimationFrame(() => {
onCellLayout();
});
}
// onLayout isn't reliably called when a cell's index shifts (e.g. an item above it
// was removed) without its own dimensions changing, so we manually re-measure. This
// was previously gated to web only, but native FlatList cells left stale, un-refreshed
// offset/size measurements after a reflow just the same — corrupting drag-reorder
// position math for cells below a deleted item.
requestAnimationFrame(() => {
onCellLayout();
});
}, [index, onCellLayout]);

const baseStyle = useMemo(() => {
Expand Down
13 changes: 12 additions & 1 deletion src/components/DraggableFlatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,22 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
}, [activeKey]);

useLayoutEffect(() => {
const currentKeys = new Set<string>();
props.data.forEach((d, i) => {
const key = keyExtractor(d, i);
currentKeys.add(key);
keyToIndexRef.current.set(key, i);
});
}, [props.data, keyExtractor, keyToIndexRef]);
// Clean up entries for items no longer in `data` (e.g. removed via delete) so a
// removed row's stale offset/size measurement doesn't stick around and corrupt
// drag-reorder position math for the remaining items.
Array.from(keyToIndexRef.current.keys()).forEach((key) => {
if (!currentKeys.has(key)) {
keyToIndexRef.current.delete(key);
cellDataRef.current.delete(key);
}
});
}, [props.data, keyExtractor, keyToIndexRef, cellDataRef]);

const drag = useStableCallback((activeKey: string) => {
if (disabled.value) return;
Expand Down
19 changes: 12 additions & 7 deletions src/hooks/useCellTranslate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ export function useCellTranslate({ cellIndex, cellSize, cellOffset }: Params) {
spacerIndexAnim,
placeholderOffset,
hoverAnim,
viewableIndexMin,
viewableIndexMax,
} = useAnimatedValues();

const { activeKey } = useDraggableFlatListContext();
Expand All @@ -27,11 +25,18 @@ export function useCellTranslate({ cellIndex, cellSize, cellOffset }: Params) {

const translate = useDerivedValue(() => {
const isActiveCell = cellIndex === activeIndexAnim.value;
const isOutsideViewableRange =
!isActiveCell &&
(cellIndex < viewableIndexMin.value ||
cellIndex > viewableIndexMax.value);
if (!activeKey || activeIndexAnim.value < 0 || isOutsideViewableRange) {
// NOTE: the upstream isOutsideViewableRange check (bailing out for cells outside
// [viewableIndexMin, viewableIndexMax]) is intentionally removed here. FlatList's
// viewability tracking is driven by onScroll + initial layout and does not
// automatically re-fire when a container grows to reveal more content without an
// actual scroll event — which is exactly our case (the queue sheet expands via an
// outer animated height, not by scrolling the inner list). That left
// viewableIndexMax permanently stuck at whatever was visible in the very first
// (collapsed, barely-visible) layout pass, silently excluding every cell beyond it
// from ever being a valid drag target. Since our queue is always a short,
// fully-rendered list (never meaningfully virtualized), this optimization isn't
// needed and was actively breaking drag-to-reorder.
if (!activeKey || activeIndexAnim.value < 0) {
return 0;
}

Expand Down