Skip to content

Commit b0c9ab7

Browse files
committed
Fixing Reorder types
1 parent 5593dc9 commit b0c9ab7

File tree

4 files changed

+168
-24
lines changed

4 files changed

+168
-24
lines changed

packages/framer-motion/src/components/Reorder/Group.tsx

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@ import * as React from "react"
55
import { forwardRef, FunctionComponent, useEffect, useRef } from "react"
66
import { ReorderContext } from "../../context/ReorderContext"
77
import { motion } from "../../render/components/motion/proxy"
8-
import type { HTMLElements } from "../../render/html/supported-elements"
98
import { HTMLMotionProps } from "../../render/html/types"
109
import { useConstant } from "../../utils/use-constant"
11-
import { ItemData, ReorderContextProps } from "./types"
10+
import {
11+
DefaultGroupElement,
12+
ItemData,
13+
ReorderContextProps,
14+
ReorderElementTag,
15+
} from "./types"
1216
import { checkReorder } from "./utils/check-reorder"
1317

14-
export interface Props<V> {
18+
export interface Props<
19+
V,
20+
TagName extends ReorderElementTag = DefaultGroupElement
21+
> {
1522
/**
1623
* A HTML element to render this component as. Defaults to `"ul"`.
1724
*
1825
* @public
1926
*/
20-
as?: keyof HTMLElements
27+
as?: TagName
2128

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

58-
type ReorderGroupProps<V> = Props<V> &
59-
Omit<HTMLMotionProps<any>, "values"> &
65+
type ReorderGroupProps<
66+
V,
67+
TagName extends ReorderElementTag = DefaultGroupElement
68+
> = Props<V, TagName> &
69+
Omit<HTMLMotionProps<TagName>, "values"> &
6070
React.PropsWithChildren<{}>
6171

62-
export function ReorderGroupComponent<V>(
72+
export function ReorderGroupComponent<
73+
V,
74+
TagName extends ReorderElementTag = DefaultGroupElement
75+
>(
6376
{
6477
children,
65-
as = "ul",
78+
as = "ul" as TagName,
6679
axis = "y",
6780
onReorder,
6881
values,
6982
...props
70-
}: ReorderGroupProps<V>,
83+
}: ReorderGroupProps<V, TagName>,
7184
externalRef?: React.ForwardedRef<any>
72-
) {
85+
): JSX.Element {
7386
const Component = useConstant(
7487
() => motion[as as keyof typeof motion]
7588
) as FunctionComponent<
@@ -127,9 +140,10 @@ export function ReorderGroupComponent<V>(
127140
}
128141

129142
export const ReorderGroup = /*@__PURE__*/ forwardRef(ReorderGroupComponent) as <
130-
V
143+
V,
144+
TagName extends ReorderElementTag = DefaultGroupElement
131145
>(
132-
props: ReorderGroupProps<V> & { ref?: React.ForwardedRef<any> }
146+
props: ReorderGroupProps<V, TagName> & { ref?: React.ForwardedRef<any> }
133147
) => ReturnType<typeof ReorderGroupComponent>
134148

135149
function getValue<V>(item: ItemData<V>) {

packages/framer-motion/src/components/Reorder/Item.tsx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,23 @@ import * as React from "react"
66
import { forwardRef, FunctionComponent, useContext } from "react"
77
import { ReorderContext } from "../../context/ReorderContext"
88
import { motion } from "../../render/components/motion/proxy"
9-
import { HTMLElements } from "../../render/html/supported-elements"
109
import { HTMLMotionProps } from "../../render/html/types"
1110
import { useConstant } from "../../utils/use-constant"
1211
import { useMotionValue } from "../../value/use-motion-value"
1312
import { useTransform } from "../../value/use-transform"
1413

15-
export interface Props<V> {
14+
import { DefaultItemElement, ReorderElementTag } from "./types"
15+
16+
export interface Props<
17+
V,
18+
TagName extends ReorderElementTag = DefaultItemElement
19+
> {
1620
/**
1721
* A HTML element to render this component as. Defaults to `"li"`.
1822
*
1923
* @public
2024
*/
21-
as?: keyof HTMLElements
25+
as?: TagName
2226

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

43-
type ReorderItemProps<V> = Props<V> &
44-
Omit<HTMLMotionProps<any>, "value" | "layout"> &
47+
type ReorderItemProps<
48+
V,
49+
TagName extends ReorderElementTag = DefaultItemElement
50+
> = Props<V, TagName> &
51+
Omit<HTMLMotionProps<TagName>, "value" | "layout"> &
4552
React.PropsWithChildren<{}>
4653

47-
export function ReorderItemComponent<V>(
54+
export function ReorderItemComponent<
55+
V,
56+
TagName extends ReorderElementTag = DefaultItemElement
57+
>(
4858
{
4959
children,
5060
style = {},
5161
value,
52-
as = "li",
62+
as = "li" as TagName,
5363
onDrag,
5464
layout = true,
5565
...props
56-
}: ReorderItemProps<V>,
66+
}: ReorderItemProps<V, TagName>,
5767
externalRef?: React.ForwardedRef<any>
58-
) {
68+
): JSX.Element {
5969
const Component = useConstant(
6070
() => motion[as as keyof typeof motion]
6171
) as FunctionComponent<
@@ -104,7 +114,8 @@ export function ReorderItemComponent<V>(
104114
}
105115

106116
export const ReorderItem = /*@__PURE__*/ forwardRef(ReorderItemComponent) as <
107-
V
117+
V,
118+
TagName extends ReorderElementTag = DefaultItemElement
108119
>(
109-
props: ReorderItemProps<V> & { ref?: React.ForwardedRef<any> }
120+
props: ReorderItemProps<V, TagName> & { ref?: React.ForwardedRef<any> }
110121
) => ReturnType<typeof ReorderItemComponent>

packages/framer-motion/src/components/Reorder/__tests__/server.ssr.test.tsx

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe("Reorder", () => {
2121

2222
it("onReorder is typed correctly", () => {
2323
const Component = () => {
24-
const [_items, setItems] = useState(["a"])
24+
const [, setItems] = useState(["a"])
2525
return (
2626
<Reorder.Group as="article" onReorder={setItems} values={[]}>
2727
<Reorder.Item as="main" value={0} />
@@ -37,4 +37,115 @@ describe("Reorder", () => {
3737
expect(staticMarkup).toBe(expectedMarkup)
3838
expect(string).toBe(expectedMarkup)
3939
})
40+
41+
it("HTML props have incorrect types - these should fail TypeScript checking", () => {
42+
// Test correct HTML props work
43+
const CorrectComponent = () => (
44+
<Reorder.Group
45+
as="article"
46+
onReorder={() => {}}
47+
values={[]}
48+
className="test-class"
49+
id="test-id"
50+
style={{ color: "red" }}
51+
onClick={() => {}}
52+
data-testid="reorder-group"
53+
>
54+
<Reorder.Item
55+
as="main"
56+
value={0}
57+
className="item-class"
58+
id="item-id"
59+
style={{ margin: "10px" }}
60+
onClick={() => {}}
61+
data-testid="reorder-item"
62+
/>
63+
</Reorder.Group>
64+
)
65+
66+
expect(() => renderToStaticMarkup(<CorrectComponent />)).not.toThrow()
67+
68+
// These incorrect prop types should cause TypeScript errors but currently don't
69+
// The @ts-expect-error comments demonstrate the type system failure
70+
71+
// Group with incorrect prop types - these should be TypeScript errors
72+
const BadGroupComponent1 = () => (
73+
<Reorder.Group
74+
as="article"
75+
onReorder={() => {}}
76+
values={[]}
77+
// @ts-expect-error - className should be string, not number
78+
className={1}
79+
>
80+
<Reorder.Item as="main" value={0} />
81+
</Reorder.Group>
82+
)
83+
84+
const BadGroupComponent2 = () => (
85+
<Reorder.Group
86+
as="article"
87+
onReorder={() => {}}
88+
values={[]}
89+
// @ts-expect-error - id should be string, not boolean
90+
id={true}
91+
>
92+
<Reorder.Item as="main" value={0} />
93+
</Reorder.Group>
94+
)
95+
96+
const BadGroupComponent3 = () => (
97+
<Reorder.Group
98+
as="article"
99+
onReorder={() => {}}
100+
values={[]}
101+
// @ts-expect-error - onClick should be function, not string
102+
onClick="test"
103+
>
104+
<Reorder.Item as="main" value={0} />
105+
</Reorder.Group>
106+
)
107+
108+
// Item with incorrect prop types - these should be TypeScript errors
109+
const BadItemComponent1 = () => (
110+
<Reorder.Group as="article" onReorder={() => {}} values={[]}>
111+
<Reorder.Item
112+
as="main"
113+
value={0}
114+
// @ts-expect-error - className should be string, not number
115+
className={1}
116+
/>
117+
</Reorder.Group>
118+
)
119+
120+
const BadItemComponent2 = () => (
121+
<Reorder.Group as="article" onReorder={() => {}} values={[]}>
122+
<Reorder.Item
123+
as="main"
124+
value={0}
125+
// @ts-expect-error - style should be object, not string
126+
style="invalid-style"
127+
/>
128+
</Reorder.Group>
129+
)
130+
131+
const BadItemComponent3 = () => (
132+
<Reorder.Group as="article" onReorder={() => {}} values={[]}>
133+
<Reorder.Item
134+
as="main"
135+
value={0}
136+
// @ts-expect-error - onClick should be function, not string
137+
onClick="test"
138+
/>
139+
</Reorder.Group>
140+
)
141+
142+
// These components demonstrate that the type system isn't catching HTML prop type errors
143+
// In a properly typed system, the above components would fail to compile
144+
expect(() => renderToStaticMarkup(<BadGroupComponent1 />)).not.toThrow()
145+
expect(() => renderToStaticMarkup(<BadGroupComponent2 />)).not.toThrow()
146+
expect(() => renderToStaticMarkup(<BadGroupComponent3 />)).not.toThrow()
147+
expect(() => renderToStaticMarkup(<BadItemComponent1 />)).not.toThrow()
148+
expect(() => renderToStaticMarkup(<BadItemComponent2 />)).not.toThrow()
149+
expect(() => renderToStaticMarkup(<BadItemComponent3 />)).not.toThrow()
150+
})
40151
})

packages/framer-motion/src/components/Reorder/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Axis, Box } from "motion-utils"
2+
import { HTMLElements } from "../../render/html/supported-elements"
23

34
export interface ReorderContextProps<T> {
45
axis: "x" | "y"
@@ -10,3 +11,10 @@ export interface ItemData<T> {
1011
value: T
1112
layout: Axis
1213
}
14+
15+
// Reorder component type helpers
16+
export type ReorderElementTag = keyof HTMLElements
17+
18+
// Default elements for each component
19+
export type DefaultGroupElement = "ul"
20+
export type DefaultItemElement = "li"

0 commit comments

Comments
 (0)