Make tiles draggable (but not yet droppable)

This commit is contained in:
Robin Townsend 2023-02-01 00:17:22 -05:00
parent d7db845f3b
commit eedf8a6d1b

View File

@ -1,6 +1,13 @@
import { SpringRef, TransitionFn, useTransition } from "@react-spring/web"; import { SpringRef, TransitionFn, useTransition } from "@react-spring/web";
import { useDrag } from "@use-gesture/react"; import { useDrag, useScroll } from "@use-gesture/react";
import React, { FC, ReactNode, useEffect, useMemo, useState } from "react"; import React, {
FC,
ReactNode,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import styles from "./NewVideoGrid.module.css"; import styles from "./NewVideoGrid.module.css";
import { TileDescriptor } from "./TileDescriptor"; import { TileDescriptor } from "./TileDescriptor";
@ -50,12 +57,19 @@ interface TileSpring {
opacity: number; opacity: number;
scale: number; scale: number;
shadow: number; shadow: number;
zIndex: number;
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
} }
interface DragState {
tileId: string;
x: number;
y: number;
}
const dijkstra = (g: Grid): number[] => { const dijkstra = (g: Grid): number[] => {
const end = findLast1By1Index(g) ?? 0; const end = findLast1By1Index(g) ?? 0;
const endRow = row(end, g); const endRow = row(end, g);
@ -461,7 +475,11 @@ export const NewVideoGrid: FC<Props> = ({
[slotRects, grid, slotGridGeneration] [slotRects, grid, slotGridGeneration]
); );
const [tileTransitions] = useTransition( // Drag state is stored in a ref rather than component state, because we use
// react-spring's imperative API during gestures to improve responsiveness
const dragState = useRef<DragState | null>(null);
const [tileTransitions, springRef] = useTransition(
tiles, tiles,
() => ({ () => ({
key: ({ item }: Tile) => item.id, key: ({ item }: Tile) => item.id,
@ -469,20 +487,26 @@ export const NewVideoGrid: FC<Props> = ({
opacity: 0, opacity: 0,
scale: 0, scale: 0,
shadow: 1, shadow: 1,
zIndex: 1,
x, x,
y, y,
width, width,
height, height,
immediate: disableAnimations,
}), }),
enter: { opacity: 1, scale: 1 }, enter: { opacity: 1, scale: 1, immediate: disableAnimations },
update: ({ x, y, width, height }: Tile) => ({ x, y, width, height }), update: ({ item, x, y, width, height }: Tile) =>
leave: { opacity: 0, scale: 0 }, item.id === dragState.current?.tileId
? {}
: {
x,
y,
width,
height,
immediate: disableAnimations,
},
leave: { opacity: 0, scale: 0, immediate: disableAnimations },
config: { mass: 0.7, tension: 252, friction: 25 }, config: { mass: 0.7, tension: 252, friction: 25 },
immediate: (key: string) =>
disableAnimations || key === "zIndex" || key === "shadow",
// If we just stopped dragging a tile, give it time for the
// animation to settle before pushing its z-index back down
delay: (key: string) => (key === "zIndex" ? 500 : 0),
}), }),
[tiles, disableAnimations] [tiles, disableAnimations]
// react-spring's types are bugged and can't infer the spring type // react-spring's types are bugged and can't infer the spring type
@ -525,20 +549,81 @@ export const NewVideoGrid: FC<Props> = ({
}; };
}, [grid]); }, [grid]);
const animateDraggedTile = (endOfGesture: boolean) =>
springRef.start((_i, controller) => {
const { tileId, x, y } = dragState.current!;
// react-spring appears to not update a controller's item as long as the
// key remains stable, so we can use it to look up the tile's ID but not
// its position
if ((controller.item as Tile).item.id === tileId) {
if (endOfGesture) {
const tile = tiles.find((t) => t.item.id === tileId)!;
return {
scale: 1,
zIndex: 1,
shadow: 1,
x: tile.x,
y: tile.y,
immediate: disableAnimations || ((key) => key === "zIndex"),
// Allow the tile's position to settle before pushing its
// z-index back down
delay: (key) => (key === "zIndex" ? 500 : 0),
};
} else {
return {
scale: 1.1,
zIndex: 2,
shadow: 15,
x,
y,
immediate:
disableAnimations ||
((key) => key === "zIndex" || key === "x" || key === "y"),
};
}
} else {
return {};
}
});
const bindTile = useDrag( const bindTile = useDrag(
({ event, tap, args }) => { ({ tap, args, delta: [dx, dy], last }) => {
event.preventDefault();
const tileId = args[0] as string; const tileId = args[0] as string;
if (tap) { if (tap) {
setGrid((g) => cycleTileSize(tileId, g)); setGrid((g) => cycleTileSize(tileId, g));
} else { } else {
// TODO const tileSpring = springRef.current
.find((c) => (c.item as Tile).item.id === tileId)!
.get();
if (dragState.current === null) {
dragState.current = { tileId, x: tileSpring.x, y: tileSpring.y };
}
dragState.current.x += dx;
dragState.current.y += dy;
animateDraggedTile(last);
if (last) dragState.current = null;
} }
}, },
{ filterTaps: true, pointer: { buttons: [1] } } { filterTaps: true, pointer: { buttons: [1] } }
); );
const scrollOffset = useRef(0);
const bindGrid = useScroll(({ xy: [, y], delta: [, dy] }) => {
scrollOffset.current = y;
if (dragState.current !== null) {
dragState.current.y += dy;
animateDraggedTile(false);
}
});
const slots = useMemo(() => { const slots = useMemo(() => {
const slots = new Array<ReactNode>(items.length); const slots = new Array<ReactNode>(items.length);
for (let i = 0; i < items.length; i++) for (let i = 0; i < items.length; i++)
@ -554,7 +639,7 @@ export const NewVideoGrid: FC<Props> = ({
} }
return ( return (
<div ref={gridRef} className={styles.grid}> <div {...bindGrid()} ref={gridRef} className={styles.grid}>
<div <div
style={slotGridStyle} style={slotGridStyle}
ref={setSlotGrid} ref={setSlotGrid}