Skip to content
Merged
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
38 changes: 26 additions & 12 deletions packages/framer-motion/src/components/Reorder/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@ import * as React from "react"
import { forwardRef, FunctionComponent, useEffect, useRef } from "react"
import { ReorderContext } from "../../context/ReorderContext"
import { motion } from "../../render/components/motion/proxy"
import type { HTMLElements } from "../../render/html/supported-elements"
import { HTMLMotionProps } from "../../render/html/types"
import { useConstant } from "../../utils/use-constant"
import { ItemData, ReorderContextProps } from "./types"
import {
DefaultGroupElement,
ItemData,
ReorderContextProps,
ReorderElementTag,
} from "./types"
import { checkReorder } from "./utils/check-reorder"

export interface Props<V> {
export interface Props<
V,
TagName extends ReorderElementTag = DefaultGroupElement
> {
/**
* A HTML element to render this component as. Defaults to `"ul"`.
*
* @public
*/
as?: keyof HTMLElements
as?: TagName

/**
* The axis to reorder along. By default, items will be draggable on this axis.
Expand Down Expand Up @@ -55,21 +62,27 @@ export interface Props<V> {
values: V[]
}

type ReorderGroupProps<V> = Props<V> &
Omit<HTMLMotionProps<any>, "values"> &
type ReorderGroupProps<
V,
TagName extends ReorderElementTag = DefaultGroupElement
> = Props<V, TagName> &
Omit<HTMLMotionProps<TagName>, "values"> &
React.PropsWithChildren<{}>

export function ReorderGroupComponent<V>(
export function ReorderGroupComponent<
V,
TagName extends ReorderElementTag = DefaultGroupElement
>(
{
children,
as = "ul",
as = "ul" as TagName,
axis = "y",
onReorder,
values,
...props
}: ReorderGroupProps<V>,
}: ReorderGroupProps<V, TagName>,
externalRef?: React.ForwardedRef<any>
) {
): JSX.Element {
const Component = useConstant(
() => motion[as as keyof typeof motion]
) as FunctionComponent<
Expand Down Expand Up @@ -127,9 +140,10 @@ export function ReorderGroupComponent<V>(
}

export const ReorderGroup = /*@__PURE__*/ forwardRef(ReorderGroupComponent) as <
V
V,
TagName extends ReorderElementTag = DefaultGroupElement
>(
props: ReorderGroupProps<V> & { ref?: React.ForwardedRef<any> }
props: ReorderGroupProps<V, TagName> & { ref?: React.ForwardedRef<any> }
) => ReturnType<typeof ReorderGroupComponent>

function getValue<V>(item: ItemData<V>) {
Expand Down
33 changes: 22 additions & 11 deletions packages/framer-motion/src/components/Reorder/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ import * as React from "react"
import { forwardRef, FunctionComponent, useContext } from "react"
import { ReorderContext } from "../../context/ReorderContext"
import { motion } from "../../render/components/motion/proxy"
import { HTMLElements } from "../../render/html/supported-elements"
import { HTMLMotionProps } from "../../render/html/types"
import { useConstant } from "../../utils/use-constant"
import { useMotionValue } from "../../value/use-motion-value"
import { useTransform } from "../../value/use-transform"

export interface Props<V> {
import { DefaultItemElement, ReorderElementTag } from "./types"

export interface Props<
V,
TagName extends ReorderElementTag = DefaultItemElement
> {
/**
* A HTML element to render this component as. Defaults to `"li"`.
*
* @public
*/
as?: keyof HTMLElements
as?: TagName

/**
* The value in the list that this component represents.
Expand All @@ -40,22 +44,28 @@ function useDefaultMotionValue(value: any, defaultValue: number = 0) {
return isMotionValue(value) ? value : useMotionValue(defaultValue)
}

type ReorderItemProps<V> = Props<V> &
Omit<HTMLMotionProps<any>, "value" | "layout"> &
type ReorderItemProps<
V,
TagName extends ReorderElementTag = DefaultItemElement
> = Props<V, TagName> &
Omit<HTMLMotionProps<TagName>, "value" | "layout"> &
React.PropsWithChildren<{}>

export function ReorderItemComponent<V>(
export function ReorderItemComponent<
V,
TagName extends ReorderElementTag = DefaultItemElement
>(
{
children,
style = {},
value,
as = "li",
as = "li" as TagName,
onDrag,
layout = true,
...props
}: ReorderItemProps<V>,
}: ReorderItemProps<V, TagName>,
externalRef?: React.ForwardedRef<any>
) {
): JSX.Element {
const Component = useConstant(
() => motion[as as keyof typeof motion]
) as FunctionComponent<
Expand Down Expand Up @@ -104,7 +114,8 @@ export function ReorderItemComponent<V>(
}

export const ReorderItem = /*@__PURE__*/ forwardRef(ReorderItemComponent) as <
V
V,
TagName extends ReorderElementTag = DefaultItemElement
>(
props: ReorderItemProps<V> & { ref?: React.ForwardedRef<any> }
props: ReorderItemProps<V, TagName> & { ref?: React.ForwardedRef<any> }
) => ReturnType<typeof ReorderItemComponent>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("Reorder", () => {

it("onReorder is typed correctly", () => {
const Component = () => {
const [_items, setItems] = useState(["a"])
const [, setItems] = useState(["a"])
return (
<Reorder.Group as="article" onReorder={setItems} values={[]}>
<Reorder.Item as="main" value={0} />
Expand All @@ -37,4 +37,115 @@ describe("Reorder", () => {
expect(staticMarkup).toBe(expectedMarkup)
expect(string).toBe(expectedMarkup)
})

it("HTML props have incorrect types - these should fail TypeScript checking", () => {
// Test correct HTML props work
const CorrectComponent = () => (
<Reorder.Group
as="article"
onReorder={() => {}}
values={[]}
className="test-class"
id="test-id"
style={{ color: "red" }}
onClick={() => {}}
data-testid="reorder-group"
>
<Reorder.Item
as="main"
value={0}
className="item-class"
id="item-id"
style={{ margin: "10px" }}
onClick={() => {}}
data-testid="reorder-item"
/>
</Reorder.Group>
)

expect(() => renderToStaticMarkup(<CorrectComponent />)).not.toThrow()

// These incorrect prop types should cause TypeScript errors but currently don't
// The @ts-expect-error comments demonstrate the type system failure

// Group with incorrect prop types - these should be TypeScript errors
const BadGroupComponent1 = () => (
<Reorder.Group
as="article"
onReorder={() => {}}
values={[]}
// @ts-expect-error - className should be string, not number
className={1}
>
<Reorder.Item as="main" value={0} />
</Reorder.Group>
)

const BadGroupComponent2 = () => (
<Reorder.Group
as="article"
onReorder={() => {}}
values={[]}
// @ts-expect-error - id should be string, not boolean
id={true}
>
<Reorder.Item as="main" value={0} />
</Reorder.Group>
)

const BadGroupComponent3 = () => (
<Reorder.Group
as="article"
onReorder={() => {}}
values={[]}
// @ts-expect-error - onClick should be function, not string
onClick="test"
>
<Reorder.Item as="main" value={0} />
</Reorder.Group>
)

// Item with incorrect prop types - these should be TypeScript errors
const BadItemComponent1 = () => (
<Reorder.Group as="article" onReorder={() => {}} values={[]}>
<Reorder.Item
as="main"
value={0}
// @ts-expect-error - className should be string, not number
className={1}
/>
</Reorder.Group>
)

const BadItemComponent2 = () => (
<Reorder.Group as="article" onReorder={() => {}} values={[]}>
<Reorder.Item
as="main"
value={0}
// @ts-expect-error - style should be object, not string
style="invalid-style"
/>
</Reorder.Group>
)

const BadItemComponent3 = () => (
<Reorder.Group as="article" onReorder={() => {}} values={[]}>
<Reorder.Item
as="main"
value={0}
// @ts-expect-error - onClick should be function, not string
onClick="test"
/>
</Reorder.Group>
)

// These components demonstrate that the type system isn't catching HTML prop type errors
// In a properly typed system, the above components would fail to compile
expect(() => renderToStaticMarkup(<BadGroupComponent1 />)).not.toThrow()
expect(() => renderToStaticMarkup(<BadGroupComponent2 />)).not.toThrow()
expect(() => renderToStaticMarkup(<BadGroupComponent3 />)).not.toThrow()
expect(() => renderToStaticMarkup(<BadItemComponent1 />)).not.toThrow()
expect(() => renderToStaticMarkup(<BadItemComponent2 />)).not.toThrow()
expect(() => renderToStaticMarkup(<BadItemComponent3 />)).not.toThrow()
})
})
8 changes: 8 additions & 0 deletions packages/framer-motion/src/components/Reorder/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Axis, Box } from "motion-utils"
import { HTMLElements } from "../../render/html/supported-elements"

export interface ReorderContextProps<T> {
axis: "x" | "y"
Expand All @@ -10,3 +11,10 @@ export interface ItemData<T> {
value: T
layout: Axis
}

// Reorder component type helpers
export type ReorderElementTag = keyof HTMLElements

// Default elements for each component
export type DefaultGroupElement = "ul"
export type DefaultItemElement = "li"