2024-05-03 06:44:36 +08:00
|
|
|
/*
|
2024-09-06 16:22:13 +08:00
|
|
|
Copyright 2023, 2024 New Vector Ltd.
|
2024-05-03 06:44:36 +08:00
|
|
|
|
2024-09-06 16:22:13 +08:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
Please see LICENSE in the repository root for full details.
|
2024-05-03 06:44:36 +08:00
|
|
|
*/
|
|
|
|
|
|
|
|
import {
|
|
|
|
SpringRef,
|
|
|
|
TransitionFn,
|
|
|
|
animated,
|
|
|
|
useTransition,
|
|
|
|
} from "@react-spring/web";
|
|
|
|
import { EventTypes, Handler, useScroll } from "@use-gesture/react";
|
|
|
|
import {
|
|
|
|
CSSProperties,
|
|
|
|
ComponentProps,
|
|
|
|
ComponentType,
|
2024-07-25 04:57:20 +08:00
|
|
|
Dispatch,
|
2024-05-03 06:44:36 +08:00
|
|
|
FC,
|
|
|
|
LegacyRef,
|
|
|
|
ReactNode,
|
2024-07-25 04:57:20 +08:00
|
|
|
SetStateAction,
|
|
|
|
createContext,
|
|
|
|
forwardRef,
|
|
|
|
memo,
|
|
|
|
useContext,
|
2024-05-03 06:44:36 +08:00
|
|
|
useEffect,
|
|
|
|
useMemo,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from "react";
|
|
|
|
import useMeasure from "react-use-measure";
|
|
|
|
import classNames from "classnames";
|
2024-09-10 15:49:35 +08:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2024-05-03 06:44:36 +08:00
|
|
|
|
|
|
|
import styles from "./Grid.module.css";
|
|
|
|
import { useMergedRefs } from "../useMergedRefs";
|
|
|
|
import { TileWrapper } from "./TileWrapper";
|
|
|
|
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
2024-05-31 01:06:24 +08:00
|
|
|
import { useInitial } from "../useInitial";
|
2024-05-03 06:44:36 +08:00
|
|
|
|
|
|
|
interface Rect {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
}
|
|
|
|
|
2024-05-31 01:06:24 +08:00
|
|
|
interface Tile<Model> {
|
2024-05-03 06:44:36 +08:00
|
|
|
id: string;
|
|
|
|
model: Model;
|
2024-05-31 01:06:24 +08:00
|
|
|
onDrag: DragCallback | undefined;
|
2024-05-03 06:44:36 +08:00
|
|
|
}
|
|
|
|
|
2024-05-31 01:06:24 +08:00
|
|
|
type PlacedTile<Model> = Tile<Model> & Rect;
|
|
|
|
|
2024-05-03 06:44:36 +08:00
|
|
|
interface TileSpring {
|
|
|
|
opacity: number;
|
|
|
|
scale: number;
|
|
|
|
zIndex: number;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
}
|
|
|
|
|
2024-06-08 05:29:48 +08:00
|
|
|
interface TileSpringUpdate extends Partial<TileSpring> {
|
|
|
|
from?: Partial<TileSpring>;
|
|
|
|
reset?: boolean;
|
|
|
|
immediate?: boolean | ((key: string) => boolean);
|
|
|
|
delay?: (key: string) => number;
|
|
|
|
}
|
|
|
|
|
2024-05-03 06:44:36 +08:00
|
|
|
interface DragState {
|
|
|
|
tileId: string;
|
|
|
|
tileX: number;
|
|
|
|
tileY: number;
|
|
|
|
cursorX: number;
|
|
|
|
cursorY: number;
|
|
|
|
}
|
|
|
|
|
2024-05-31 01:06:24 +08:00
|
|
|
interface SlotProps<Model> extends Omit<ComponentProps<"div">, "onDrag"> {
|
|
|
|
id: string;
|
|
|
|
model: Model;
|
|
|
|
onDrag?: DragCallback;
|
2024-05-03 06:44:36 +08:00
|
|
|
style?: CSSProperties;
|
|
|
|
className?: string;
|
|
|
|
}
|
|
|
|
|
2024-05-22 05:05:37 +08:00
|
|
|
interface Offset {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the offset of one element relative to an ancestor.
|
|
|
|
*/
|
|
|
|
function offset(element: HTMLElement, relativeTo: Element): Offset {
|
|
|
|
if (
|
|
|
|
!(element.offsetParent instanceof HTMLElement) ||
|
|
|
|
element.offsetParent === relativeTo
|
|
|
|
) {
|
|
|
|
return { x: element.offsetLeft, y: element.offsetTop };
|
|
|
|
} else {
|
|
|
|
const o = offset(element.offsetParent, relativeTo);
|
|
|
|
o.x += element.offsetLeft;
|
|
|
|
o.y += element.offsetTop;
|
|
|
|
return o;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-25 04:57:20 +08:00
|
|
|
interface LayoutContext {
|
|
|
|
setGeneration: Dispatch<SetStateAction<number | null>>;
|
|
|
|
}
|
|
|
|
|
|
|
|
const LayoutContext = createContext<LayoutContext | null>(null);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enables Grid to react to layout changes. You must call this in your Layout
|
|
|
|
* component or else Grid will not be reactive.
|
|
|
|
*/
|
2024-07-26 00:50:28 +08:00
|
|
|
export function useUpdateLayout(): void {
|
2024-07-25 04:57:20 +08:00
|
|
|
const context = useContext(LayoutContext);
|
|
|
|
if (context === null)
|
2024-07-26 00:50:28 +08:00
|
|
|
throw new Error("useUpdateLayout called outside a Grid layout context");
|
2024-07-25 04:57:20 +08:00
|
|
|
|
|
|
|
// On every render, tell Grid that the layout may have changed
|
|
|
|
useEffect(() =>
|
|
|
|
context.setGeneration((prev) => (prev === null ? 0 : prev + 1)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-05-31 01:06:24 +08:00
|
|
|
export interface LayoutProps<LayoutModel, TileModel, R extends HTMLElement> {
|
2024-05-03 06:44:36 +08:00
|
|
|
ref: LegacyRef<R>;
|
2024-05-31 01:06:24 +08:00
|
|
|
model: LayoutModel;
|
|
|
|
/**
|
|
|
|
* Component creating an invisible "slot" for a tile to go in.
|
|
|
|
*/
|
|
|
|
Slot: ComponentType<SlotProps<TileModel>>;
|
2024-05-03 06:44:36 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface TileProps<Model, R extends HTMLElement> {
|
|
|
|
ref: LegacyRef<R>;
|
|
|
|
className?: string;
|
|
|
|
style?: ComponentProps<typeof animated.div>["style"];
|
|
|
|
/**
|
|
|
|
* The width this tile will have once its animations have settled.
|
|
|
|
*/
|
|
|
|
targetWidth: number;
|
|
|
|
/**
|
|
|
|
* The height this tile will have once its animations have settled.
|
|
|
|
*/
|
|
|
|
targetHeight: number;
|
|
|
|
model: Model;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Drag {
|
|
|
|
/**
|
|
|
|
* The X coordinate of the dragged tile in grid space.
|
|
|
|
*/
|
|
|
|
x: number;
|
|
|
|
/**
|
|
|
|
* The Y coordinate of the dragged tile in grid space.
|
|
|
|
*/
|
|
|
|
y: number;
|
|
|
|
/**
|
|
|
|
* The X coordinate of the dragged tile, as a scalar of the grid width.
|
|
|
|
*/
|
|
|
|
xRatio: number;
|
|
|
|
/**
|
|
|
|
* The Y coordinate of the dragged tile, as a scalar of the grid height.
|
|
|
|
*/
|
|
|
|
yRatio: number;
|
|
|
|
}
|
|
|
|
|
2024-05-31 01:06:24 +08:00
|
|
|
export type DragCallback = (drag: Drag) => void;
|
2024-05-03 06:44:36 +08:00
|
|
|
|
2024-07-25 04:57:20 +08:00
|
|
|
interface LayoutMemoProps<LayoutModel, TileModel, R extends HTMLElement>
|
|
|
|
extends LayoutProps<LayoutModel, TileModel, R> {
|
|
|
|
Layout: ComponentType<LayoutProps<LayoutModel, TileModel, R>>;
|
|
|
|
}
|
|
|
|
|
2024-05-03 06:44:36 +08:00
|
|
|
interface Props<
|
|
|
|
LayoutModel,
|
|
|
|
TileModel,
|
|
|
|
LayoutRef extends HTMLElement,
|
|
|
|
TileRef extends HTMLElement,
|
|
|
|
> {
|
|
|
|
/**
|
|
|
|
* Data with which to populate the layout.
|
|
|
|
*/
|
|
|
|
model: LayoutModel;
|
|
|
|
/**
|
2024-05-31 01:06:24 +08:00
|
|
|
* A component which creates an invisible layout grid of "slots" for tiles to
|
|
|
|
* go in. The root element must have a data-generation attribute which
|
|
|
|
* increments whenever the layout may have changed.
|
2024-05-03 06:44:36 +08:00
|
|
|
*/
|
2024-05-31 01:06:24 +08:00
|
|
|
Layout: ComponentType<LayoutProps<LayoutModel, TileModel, LayoutRef>>;
|
2024-05-03 06:44:36 +08:00
|
|
|
/**
|
|
|
|
* The component used to render each tile in the layout.
|
|
|
|
*/
|
|
|
|
Tile: ComponentType<TileProps<TileModel, TileRef>>;
|
|
|
|
className?: string;
|
|
|
|
style?: CSSProperties;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A grid of animated tiles.
|
|
|
|
*/
|
|
|
|
export function Grid<
|
|
|
|
LayoutModel,
|
|
|
|
TileModel,
|
|
|
|
LayoutRef extends HTMLElement,
|
|
|
|
TileRef extends HTMLElement,
|
|
|
|
>({
|
|
|
|
model,
|
2024-05-31 01:06:24 +08:00
|
|
|
Layout,
|
2024-05-03 06:44:36 +08:00
|
|
|
Tile,
|
|
|
|
className,
|
|
|
|
style,
|
|
|
|
}: Props<LayoutModel, TileModel, LayoutRef, TileRef>): ReactNode {
|
|
|
|
// Overview: This component places tiles by rendering an invisible layout grid
|
|
|
|
// of "slots" for tiles to go in. Once rendered, it uses the DOM API to get
|
|
|
|
// the dimensions of each slot, feeding these numbers back into react-spring
|
|
|
|
// to let the actual tiles move freely atop the layout.
|
|
|
|
|
|
|
|
// To tell us when the layout has changed, the layout system increments its
|
|
|
|
// data-generation attribute, which we watch with a MutationObserver.
|
|
|
|
|
|
|
|
const [gridRef1, gridBounds] = useMeasure();
|
|
|
|
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
|
|
|
|
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);
|
|
|
|
|
2024-07-26 00:50:28 +08:00
|
|
|
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
|
2024-05-03 06:44:36 +08:00
|
|
|
const [generation, setGeneration] = useState<number | null>(null);
|
2024-05-31 01:06:24 +08:00
|
|
|
const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
|
2024-05-03 06:44:36 +08:00
|
|
|
const prefersReducedMotion = usePrefersReducedMotion();
|
|
|
|
|
2024-05-31 01:06:24 +08:00
|
|
|
const Slot: FC<SlotProps<TileModel>> = useMemo(
|
|
|
|
() =>
|
|
|
|
function Slot({ id, model, onDrag, style, className, ...props }) {
|
|
|
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
|
|
tiles.set(id, { id, model, onDrag });
|
|
|
|
return (): void => void tiles.delete(id);
|
|
|
|
}, [id, model, onDrag]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
ref={ref}
|
|
|
|
className={classNames(className, styles.slot)}
|
|
|
|
data-id={id}
|
|
|
|
style={style}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
[tiles],
|
|
|
|
);
|
|
|
|
|
2024-07-25 04:57:20 +08:00
|
|
|
// We must memoize the Layout component to break the update loop where a
|
|
|
|
// render of Grid causes a re-render of Layout, which in turn re-renders Grid
|
|
|
|
const LayoutMemo = useMemo(
|
|
|
|
() =>
|
|
|
|
memo(
|
|
|
|
forwardRef<
|
|
|
|
LayoutRef,
|
|
|
|
LayoutMemoProps<LayoutModel, TileModel, LayoutRef>
|
|
|
|
>(function LayoutMemo({ Layout, ...props }, ref): ReactNode {
|
|
|
|
return <Layout {...props} ref={ref} />;
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
[],
|
2024-05-03 06:44:36 +08:00
|
|
|
);
|
|
|
|
|
2024-07-25 04:57:20 +08:00
|
|
|
const context: LayoutContext = useMemo(() => ({ setGeneration }), []);
|
2024-05-03 06:44:36 +08:00
|
|
|
|
2024-05-31 01:06:24 +08:00
|
|
|
// Combine the tile definitions and slots together to create placed tiles
|
|
|
|
const placedTiles = useMemo(() => {
|
|
|
|
const result: PlacedTile<TileModel>[] = [];
|
2024-05-03 06:44:36 +08:00
|
|
|
|
2024-05-22 05:05:37 +08:00
|
|
|
if (gridRoot !== null && layoutRoot !== null) {
|
2024-05-03 06:44:36 +08:00
|
|
|
const slots = layoutRoot.getElementsByClassName(
|
|
|
|
styles.slot,
|
|
|
|
) as HTMLCollectionOf<HTMLElement>;
|
2024-05-31 01:06:24 +08:00
|
|
|
for (const slot of slots) {
|
|
|
|
const id = slot.getAttribute("data-id")!;
|
2024-06-13 03:26:00 +08:00
|
|
|
if (slot.offsetWidth > 0 && slot.offsetHeight > 0)
|
|
|
|
result.push({
|
|
|
|
...tiles.get(id)!,
|
|
|
|
...offset(slot, gridRoot),
|
|
|
|
width: slot.offsetWidth,
|
|
|
|
height: slot.offsetHeight,
|
|
|
|
});
|
2024-05-31 01:06:24 +08:00
|
|
|
}
|
2024-05-03 06:44:36 +08:00
|
|
|
}
|
|
|
|
|
2024-05-31 01:06:24 +08:00
|
|
|
return result;
|
2024-07-25 04:57:20 +08:00
|
|
|
// The rects may change due to the grid resizing or updating to a new
|
|
|
|
// generation, but eslint can't statically verify this
|
2024-05-03 06:44:36 +08:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2024-07-25 04:57:20 +08:00
|
|
|
}, [gridRoot, layoutRoot, tiles, gridBounds, generation]);
|
2024-05-03 06:44:36 +08:00
|
|
|
|
|
|
|
// 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(
|
2024-05-31 01:06:24 +08:00
|
|
|
placedTiles,
|
2024-05-03 06:44:36 +08:00
|
|
|
() => ({
|
|
|
|
key: ({ id }: Tile<TileModel>): string => id,
|
2024-05-31 01:06:24 +08:00
|
|
|
from: ({
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
}: PlacedTile<TileModel>): TileSpringUpdate => ({
|
2024-05-03 06:44:36 +08:00
|
|
|
opacity: 0,
|
|
|
|
scale: 0,
|
|
|
|
zIndex: 1,
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
immediate: prefersReducedMotion,
|
|
|
|
}),
|
|
|
|
enter: { opacity: 1, scale: 1, immediate: prefersReducedMotion },
|
|
|
|
update: ({
|
|
|
|
id,
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
width,
|
|
|
|
height,
|
2024-05-31 01:06:24 +08:00
|
|
|
}: PlacedTile<TileModel>): TileSpringUpdate | null =>
|
2024-05-03 06:44:36 +08:00
|
|
|
id === dragState.current?.tileId
|
|
|
|
? null
|
|
|
|
: {
|
|
|
|
x,
|
|
|
|
y,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
immediate: prefersReducedMotion,
|
|
|
|
},
|
|
|
|
leave: { opacity: 0, scale: 0, immediate: prefersReducedMotion },
|
|
|
|
config: { mass: 0.7, tension: 252, friction: 25 },
|
|
|
|
}),
|
|
|
|
// react-spring's types are bugged and can't infer the spring type
|
|
|
|
) as unknown as [
|
2024-05-31 01:06:24 +08:00
|
|
|
TransitionFn<PlacedTile<TileModel>, TileSpring>,
|
2024-05-03 06:44:36 +08:00
|
|
|
SpringRef<TileSpring>,
|
|
|
|
];
|
|
|
|
|
|
|
|
// Because we're using react-spring in imperative mode, we're responsible for
|
|
|
|
// firing animations manually whenever the tiles array updates
|
|
|
|
useEffect(() => {
|
2024-09-10 15:49:35 +08:00
|
|
|
springRef.start().forEach((p) => void p.catch(logger.error));
|
2024-05-31 01:06:24 +08:00
|
|
|
}, [placedTiles, springRef]);
|
2024-05-03 06:44:36 +08:00
|
|
|
|
|
|
|
const animateDraggedTile = (
|
|
|
|
endOfGesture: boolean,
|
|
|
|
callback: DragCallback,
|
|
|
|
): void => {
|
|
|
|
const { tileId, tileX, tileY } = dragState.current!;
|
2024-05-31 01:06:24 +08:00
|
|
|
const tile = placedTiles.find((t) => t.id === tileId)!;
|
2024-05-03 06:44:36 +08:00
|
|
|
|
|
|
|
springRef.current
|
|
|
|
.find((c) => (c.item as Tile<TileModel>).id === tileId)
|
|
|
|
?.start(
|
|
|
|
endOfGesture
|
|
|
|
? {
|
|
|
|
scale: 1,
|
|
|
|
zIndex: 1,
|
|
|
|
x: tile.x,
|
|
|
|
y: tile.y,
|
|
|
|
width: tile.width,
|
|
|
|
height: tile.height,
|
|
|
|
immediate:
|
|
|
|
prefersReducedMotion || ((key): boolean => key === "zIndex"),
|
|
|
|
// Allow the tile's position to settle before pushing its
|
|
|
|
// z-index back down
|
|
|
|
delay: (key): number => (key === "zIndex" ? 500 : 0),
|
|
|
|
}
|
|
|
|
: {
|
|
|
|
scale: 1.1,
|
|
|
|
zIndex: 2,
|
|
|
|
x: tileX,
|
|
|
|
y: tileY,
|
|
|
|
immediate:
|
|
|
|
prefersReducedMotion ||
|
|
|
|
((key): boolean =>
|
|
|
|
key === "zIndex" || key === "x" || key === "y"),
|
|
|
|
},
|
2024-09-10 15:49:35 +08:00
|
|
|
)
|
|
|
|
.catch(logger.error);
|
2024-05-03 06:44:36 +08:00
|
|
|
|
|
|
|
if (endOfGesture)
|
|
|
|
callback({
|
|
|
|
x: tileX,
|
|
|
|
y: tileY,
|
|
|
|
xRatio: tileX / (gridBounds.width - tile.width),
|
|
|
|
yRatio: tileY / (gridBounds.height - tile.height),
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// Callback for useDrag. We could call useDrag here, but the default
|
|
|
|
// pattern of spreading {...bind()} across the children to bind the gesture
|
|
|
|
// ends up breaking memoization and ruining this component's performance.
|
|
|
|
// Instead, we pass this callback to each tile via a ref, to let them bind the
|
|
|
|
// gesture using the much more sensible ref-based method.
|
|
|
|
const onTileDrag = (
|
|
|
|
tileId: string,
|
|
|
|
|
|
|
|
{
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
tap,
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
initial: [initialX, initialY],
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
delta: [dx, dy],
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
last,
|
|
|
|
}: Parameters<Handler<"drag", EventTypes["drag"]>>[0],
|
|
|
|
): void => {
|
|
|
|
if (!tap) {
|
|
|
|
const tileController = springRef.current.find(
|
|
|
|
(c) => (c.item as Tile<TileModel>).id === tileId,
|
|
|
|
)!;
|
2024-05-31 01:06:24 +08:00
|
|
|
const callback = tiles.get(tileController.item.id)!.onDrag;
|
2024-05-03 06:44:36 +08:00
|
|
|
|
|
|
|
if (callback != null) {
|
|
|
|
if (dragState.current === null) {
|
|
|
|
const tileSpring = tileController.get();
|
|
|
|
dragState.current = {
|
|
|
|
tileId,
|
|
|
|
tileX: tileSpring.x,
|
|
|
|
tileY: tileSpring.y,
|
|
|
|
cursorX: initialX - gridBounds.x,
|
|
|
|
cursorY: initialY - gridBounds.y + scrollOffset.current,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
dragState.current.tileX += dx;
|
|
|
|
dragState.current.tileY += dy;
|
|
|
|
dragState.current.cursorX += dx;
|
|
|
|
dragState.current.cursorY += dy;
|
|
|
|
|
|
|
|
animateDraggedTile(last, callback);
|
|
|
|
|
|
|
|
if (last) dragState.current = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const onTileDragRef = useRef(onTileDrag);
|
|
|
|
onTileDragRef.current = onTileDrag;
|
|
|
|
|
|
|
|
const scrollOffset = useRef(0);
|
|
|
|
|
|
|
|
useScroll(
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
({ xy: [, y], delta: [, dy] }) => {
|
|
|
|
scrollOffset.current = y;
|
|
|
|
|
|
|
|
if (dragState.current !== null) {
|
|
|
|
dragState.current.tileY += dy;
|
|
|
|
dragState.current.cursorY += dy;
|
2024-05-31 01:06:24 +08:00
|
|
|
animateDraggedTile(false, tiles.get(dragState.current.tileId)!.onDrag!);
|
2024-05-03 06:44:36 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
{ target: gridRoot ?? undefined },
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
ref={gridRef}
|
|
|
|
className={classNames(className, styles.grid)}
|
|
|
|
style={style}
|
|
|
|
>
|
2024-07-25 04:57:20 +08:00
|
|
|
<LayoutContext.Provider value={context}>
|
2024-07-26 00:50:28 +08:00
|
|
|
<LayoutMemo
|
|
|
|
ref={setLayoutRoot}
|
|
|
|
Layout={Layout}
|
|
|
|
model={model}
|
|
|
|
Slot={Slot}
|
|
|
|
/>
|
2024-07-25 04:57:20 +08:00
|
|
|
</LayoutContext.Provider>
|
2024-05-31 01:06:24 +08:00
|
|
|
{tileTransitions((spring, { id, model, onDrag, width, height }) => (
|
2024-05-03 06:44:36 +08:00
|
|
|
<TileWrapper
|
|
|
|
key={id}
|
|
|
|
id={id}
|
2024-05-31 01:06:24 +08:00
|
|
|
onDrag={onDrag ? onTileDragRef : null}
|
2024-05-03 06:44:36 +08:00
|
|
|
targetWidth={width}
|
|
|
|
targetHeight={height}
|
|
|
|
model={model}
|
|
|
|
Tile={Tile}
|
|
|
|
{...spring}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|