Skip to content

Commit b67cd79

Browse files
committed
Fix drag gesture on keyboard-accessible elements with drag prop
Previously, the drag gesture was blocked on any keyboard-accessible element (button, input, select, textarea, a) regardless of whether the element itself had the drag prop. This meant motion.button with drag prop couldn't be dragged. The fix only blocks drag when clicking on a keyboard-accessible *child* element of the draggable element. If the draggable element itself is keyboard-accessible, dragging should work when clicking directly on it. Fixes #3473
1 parent eab3b0e commit b67cd79

File tree

2 files changed

+155
-5
lines changed

2 files changed

+155
-5
lines changed

packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -642,11 +642,18 @@ export class VisualElementDragControls {
642642
"pointerdown",
643643
(event) => {
644644
const { drag, dragListener = true } = this.getProps()
645-
if (
646-
drag &&
647-
dragListener &&
648-
!isElementKeyboardAccessible(event.target as Element)
649-
) {
645+
const target = event.target as Element
646+
647+
/**
648+
* Only block drag if clicking on a keyboard-accessible child element.
649+
* If the draggable element itself is keyboard-accessible (e.g., motion.button),
650+
* dragging should still work when clicking directly on it.
651+
*/
652+
const isClickingKeyboardAccessibleChild =
653+
target !== element &&
654+
isElementKeyboardAccessible(target)
655+
656+
if (drag && dragListener && !isClickingKeyboardAccessibleChild) {
650657
this.start(event)
651658
}
652659
}

packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,3 +944,146 @@ describe("dragging", () => {
944944
)
945945
})
946946
})
947+
948+
describe("keyboard accessible elements", () => {
949+
test("drag gesture starts on a motion.button with drag prop", async () => {
950+
const onDragStart = jest.fn()
951+
const x = motionValue(0)
952+
const Component = () => (
953+
<MockDrag>
954+
<motion.button
955+
data-testid="draggable-button"
956+
drag
957+
onDragStart={onDragStart}
958+
style={{ x }}
959+
/>
960+
</MockDrag>
961+
)
962+
963+
const { getByTestId, rerender } = render(<Component />)
964+
rerender(<Component />)
965+
966+
const pointer = await drag(getByTestId("draggable-button")).to(100, 100)
967+
pointer.end()
968+
969+
await nextFrame()
970+
971+
expect(onDragStart).toBeCalledTimes(1)
972+
expect(x.get()).toBe(100)
973+
})
974+
975+
test("drag gesture does not start when clicking a child button", async () => {
976+
const onDragStart = jest.fn()
977+
const x = motionValue(0)
978+
const Component = () => (
979+
<MockDrag>
980+
<motion.div
981+
data-testid="draggable"
982+
drag
983+
onDragStart={onDragStart}
984+
style={{ x }}
985+
>
986+
<button data-testid="child-button">Click me</button>
987+
</motion.div>
988+
</MockDrag>
989+
)
990+
991+
const { getByTestId, rerender } = render(<Component />)
992+
rerender(<Component />)
993+
994+
const pointer = await drag(
995+
getByTestId("draggable"),
996+
getByTestId("child-button")
997+
).to(100, 100)
998+
pointer.end()
999+
1000+
await nextFrame()
1001+
1002+
expect(onDragStart).toBeCalledTimes(0)
1003+
expect(x.get()).toBe(0)
1004+
})
1005+
1006+
test("drag gesture starts on a motion.input with drag prop", async () => {
1007+
const onDragStart = jest.fn()
1008+
const x = motionValue(0)
1009+
const Component = () => (
1010+
<MockDrag>
1011+
<motion.input
1012+
data-testid="draggable-input"
1013+
drag
1014+
onDragStart={onDragStart}
1015+
style={{ x }}
1016+
/>
1017+
</MockDrag>
1018+
)
1019+
1020+
const { getByTestId, rerender } = render(<Component />)
1021+
rerender(<Component />)
1022+
1023+
const pointer = await drag(getByTestId("draggable-input")).to(100, 100)
1024+
pointer.end()
1025+
1026+
await nextFrame()
1027+
1028+
expect(onDragStart).toBeCalledTimes(1)
1029+
expect(x.get()).toBe(100)
1030+
})
1031+
1032+
test("drag gesture starts on a motion.a with drag prop", async () => {
1033+
const onDragStart = jest.fn()
1034+
const x = motionValue(0)
1035+
const Component = () => (
1036+
<MockDrag>
1037+
<motion.a
1038+
data-testid="draggable-link"
1039+
drag
1040+
onDragStart={onDragStart}
1041+
style={{ x }}
1042+
href="#"
1043+
/>
1044+
</MockDrag>
1045+
)
1046+
1047+
const { getByTestId, rerender } = render(<Component />)
1048+
rerender(<Component />)
1049+
1050+
const pointer = await drag(getByTestId("draggable-link")).to(100, 100)
1051+
pointer.end()
1052+
1053+
await nextFrame()
1054+
1055+
expect(onDragStart).toBeCalledTimes(1)
1056+
expect(x.get()).toBe(100)
1057+
})
1058+
1059+
test("drag gesture does not start when clicking a child input", async () => {
1060+
const onDragStart = jest.fn()
1061+
const x = motionValue(0)
1062+
const Component = () => (
1063+
<MockDrag>
1064+
<motion.div
1065+
data-testid="draggable"
1066+
drag
1067+
onDragStart={onDragStart}
1068+
style={{ x }}
1069+
>
1070+
<input data-testid="child-input" />
1071+
</motion.div>
1072+
</MockDrag>
1073+
)
1074+
1075+
const { getByTestId, rerender } = render(<Component />)
1076+
rerender(<Component />)
1077+
1078+
const pointer = await drag(
1079+
getByTestId("draggable"),
1080+
getByTestId("child-input")
1081+
).to(100, 100)
1082+
pointer.end()
1083+
1084+
await nextFrame()
1085+
1086+
expect(onDragStart).toBeCalledTimes(0)
1087+
expect(x.get()).toBe(0)
1088+
})
1089+
})

0 commit comments

Comments
 (0)