Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multidimensional reorder #1862

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Expand Up @@ -30,4 +30,4 @@ yarn-error.log
.turbo
stats.html

lerna-debug.log
lerna-debug.log
140 changes: 140 additions & 0 deletions dev/examples/Reorder.tsx
@@ -0,0 +1,140 @@
import * as React from "react"
import { Reorder } from "framer-motion"
import { useState } from "react"

export const App = () => {
const [verticalGridItems, setVerticalGridItems] = useState([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26,
])
const [horizontalGridItems, setHorizontalGridItems] = useState([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
])
const [verticalItems, setVerticalItems] = useState([0, 1, 2, 3, 4])
const [horizontalItems, setHorizontalItems] = useState([0, 1])

const [reorderType, setReorderType] = useState("row-grid")

const reorder = {
"row-grid": (
<Reorder.Group
style={{
display: "flex",
flexDirection: "row",
maxWidth: "500px",
flexWrap: "wrap",
flex: 1,
}}
as="div"
values={verticalGridItems}
onReorder={setVerticalGridItems}
>
{verticalGridItems.map((item) => (
<ReorderItem item={item} key={item} />
))}
</Reorder.Group>
),
"column-grid": (
<Reorder.Group
style={{
display: "flex",
flexDirection: "column",
height: "100vh",
maxHeight: "400px",
flexWrap: "wrap",
flex: 0,
}}
as="div"
values={horizontalGridItems}
onReorder={setHorizontalGridItems}
>
{horizontalGridItems.map((item) => (
<ReorderItem item={item} key={item} />
))}
</Reorder.Group>
),
row: (
<Reorder.Group
style={{
display: "flex",
flex: 1,
}}
values={horizontalItems}
onReorder={setHorizontalItems}
>
{horizontalItems.map((item) => (
<ReorderItem
item={item}
key={item}
style={{ width: 100 * (item + 1) }}
/>
))}
</Reorder.Group>
),
column: (
<Reorder.Group
style={{
display: "flex",
flexDirection: "column",
flex: 1,
}}
values={verticalItems}
onReorder={setVerticalItems}
>
{verticalItems.map((item) => (
<ReorderItem item={item} key={item} />
))}
</Reorder.Group>
),
}

return (
<>
<div>
<div style={{ display: "flex", gap: "50px" }}>
{reorder[reorderType]}
</div>
</div>
<div
style={{
display: "flex",
position: "absolute",
bottom: 0,
justifyContent: "center",
}}
>
<button onClick={() => setReorderType("row-grid")}>
Row Grid
</button>
<button onClick={() => setReorderType("column-grid")}>
Column Grid
</button>
<button onClick={() => setReorderType("row")}>Row</button>
<button onClick={() => setReorderType("column")}>Column</button>
</div>
</>
)
}

const ReorderItem = ({
item,
style,
}: {
item: number
style?: React.CSSProperties
}) => (
<Reorder.Item
as="div"
style={{
backgroundColor: "white",
width: "50px",
height: "50px",
margin: "10px",
...style,
}}
key={item}
value={item}
>
{item}
</Reorder.Item>
)
71 changes: 37 additions & 34 deletions packages/framer-motion/src/components/Reorder/Group.tsx
Expand Up @@ -5,14 +5,16 @@ import {
FunctionComponent,
ReactHTML,
useEffect,
useMemo,
useRef,
} from "react"
import { ReorderContext } from "../../context/ReorderContext"
import { motion } from "../../render/dom/motion"
import { HTMLMotionProps } from "../../render/html/types"
import { useConstant } from "../../utils/use-constant"
import { ItemData, ReorderContextProps } from "./types"
import { ItemLayout, ReorderContextProps } from "./types"
import { checkReorder } from "./utils/check-reorder"
import { useOrderState } from "./utils/use-order-state"

export interface Props<V> {
/**
Expand All @@ -23,8 +25,10 @@ export interface Props<V> {
as?: keyof ReactHTML

/**
* The axis to reorder along. By default, items will be draggable on this axis.
* To make draggable on both axes, set `<Reorder.Item drag />`
* The axis to reorder along. By default, this is calculated dynamically.
*
* Items will visually drag only along the reorder axis if no items wrap.
* This can be overridden by setting `<Reorder.Item drag />`
*
* @public
*/
Expand Down Expand Up @@ -64,7 +68,7 @@ export function ReorderGroup<V>(
{
children,
as = "ul",
axis = "y",
axis: axisOverride,
onReorder,
values,
...props
Expand All @@ -73,41 +77,48 @@ export function ReorderGroup<V>(
React.PropsWithChildren<{}>,
externalRef?: React.Ref<any>
) {
const internalRef = useRef<HTMLElement>()
const ref = useMemo(
() => externalRef || internalRef,
[externalRef, internalRef]
) as React.MutableRefObject<any>

const Component = useConstant(() => motion(as)) as FunctionComponent<
React.PropsWithChildren<HTMLMotionProps<any> & { ref?: React.Ref<any> }>
>

const order: ItemData<V>[] = []
const itemLayouts = useConstant<ItemLayout<V>>(() => new Map())
const isReordering = useRef(false)

const { axis, isWrapping, itemsPerAxis } = useOrderState(
ref,
values,
itemLayouts,
axisOverride
)

invariant(Boolean(values), "Reorder.Group must be provided a values prop")

const context: ReorderContextProps<any> = {
axis,
registerItem: (value, layout) => {
/**
* Ensure entries can't add themselves more than once
*/
if (
layout &&
order.findIndex((entry) => value === entry.value) === -1
) {
order.push({ value, layout: layout[axis] })
order.sort(compareMin)
}
},
isWrapping,
registerItem: (value, layout) => itemLayouts.set(value, layout),
updateOrder: (id, offset, velocity) => {
if (isReordering.current) return

const newOrder = checkReorder(order, id, offset, velocity)
const newValuesOrder = checkReorder(
values,
itemLayouts,
id,
offset,
velocity,
axis,
isWrapping,
itemsPerAxis
)

if (order !== newOrder) {
if (values !== newValuesOrder) {
isReordering.current = true
onReorder(
newOrder
.map(getValue)
.filter((value) => values.indexOf(value) !== -1)
)
onReorder(newValuesOrder)
}
},
}
Expand All @@ -117,7 +128,7 @@ export function ReorderGroup<V>(
})

return (
<Component {...props} ref={externalRef}>
<Component {...props} ref={ref}>
<ReorderContext.Provider value={context}>
{children}
</ReorderContext.Provider>
Expand All @@ -126,11 +137,3 @@ export function ReorderGroup<V>(
}

export const Group = forwardRef(ReorderGroup)

function getValue<V>(item: ItemData<V>) {
return item.value
}

function compareMin<V>(a: ItemData<V>, b: ItemData<V>) {
return a.layout.min - b.layout.min
}
13 changes: 7 additions & 6 deletions packages/framer-motion/src/components/Reorder/Item.tsx
Expand Up @@ -75,24 +75,26 @@ export function ReorderItem<V>(

invariant(Boolean(context), "Reorder.Item must be a child of Reorder.Group")

const { axis, registerItem, updateOrder } = context!
const { axis, isWrapping, registerItem, updateOrder } = context!

useEffect(() => {
registerItem(value, measuredLayout.current!)
}, [context])

return (
<Component
drag={axis}
drag={isWrapping ? true : axis}
{...props}
dragSnapToOrigin
style={{ ...style, x: point.x, y: point.y, zIndex }}
layout={layout}
onDrag={(event, gesturePoint) => {
const { velocity } = gesturePoint
velocity[axis] &&
updateOrder(value, point[axis].get(), velocity[axis])

updateOrder(
value,
{ x: point.x.get(), y: point.y.get() },
{ x: velocity.x, y: velocity.y }
)
onDrag && onDrag(event, gesturePoint)
}}
onLayoutMeasure={(measured) => {
Expand All @@ -104,5 +106,4 @@ export function ReorderItem<V>(
</Component>
)
}

export const Item = forwardRef(ReorderItem)
Expand Up @@ -14,7 +14,7 @@ describe("Reorder", () => {
const staticMarkup = renderToStaticMarkup(<Component />)
const string = renderToString(<Component />)

const expectedMarkup = `<article><main style="z-index:unset;transform:none;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;touch-action:pan-x" draggable="false"></main></article>`
const expectedMarkup = `<article><main style="z-index:unset;transform:none;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;touch-action:pan-y" draggable="false"></main></article>`

expect(staticMarkup).toBe(expectedMarkup)
expect(string).toBe(expectedMarkup)
Expand All @@ -33,7 +33,7 @@ describe("Reorder", () => {
const staticMarkup = renderToStaticMarkup(<Component />)
const string = renderToString(<Component />)

const expectedMarkup = `<article><main style="z-index:unset;transform:none;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;touch-action:pan-x" draggable="false"></main></article>`
const expectedMarkup = `<article><main style="z-index:unset;transform:none;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;touch-action:pan-y" draggable="false"></main></article>`

expect(staticMarkup).toBe(expectedMarkup)
expect(string).toBe(expectedMarkup)
Expand Down
10 changes: 4 additions & 6 deletions packages/framer-motion/src/components/Reorder/types.ts
@@ -1,12 +1,10 @@
import { Axis, Box } from "../../projection/geometry/types"
import { Box, Point } from "../../projection/geometry/types"

export interface ReorderContextProps<T> {
axis: "x" | "y"
isWrapping: boolean
registerItem: (id: T, layout: Box) => void
updateOrder: (id: T, offset: number, velocity: number) => void
updateOrder: (id: T, offset: Point, velocity: Point) => void
}

export interface ItemData<T> {
value: T
layout: Axis
}
export type ItemLayout<T> = Map<T, Box>