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
Original file line number Diff line number Diff line change
Expand Up @@ -642,11 +642,18 @@ export class VisualElementDragControls {
"pointerdown",
(event) => {
const { drag, dragListener = true } = this.getProps()
if (
drag &&
dragListener &&
!isElementKeyboardAccessible(event.target as Element)
) {
const target = event.target as Element

/**
* Only block drag if clicking on a keyboard-accessible child element.
* If the draggable element itself is keyboard-accessible (e.g., motion.button),
* dragging should still work when clicking directly on it.
*/
const isClickingKeyboardAccessibleChild =
target !== element &&
isElementKeyboardAccessible(target)

if (drag && dragListener && !isClickingKeyboardAccessibleChild) {
this.start(event)
}
}
Expand Down
143 changes: 143 additions & 0 deletions packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -944,3 +944,146 @@ describe("dragging", () => {
)
})
})

describe("keyboard accessible elements", () => {
test("drag gesture starts on a motion.button with drag prop", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
<MockDrag>
<motion.button
data-testid="draggable-button"
drag
onDragStart={onDragStart}
style={{ x }}
/>
</MockDrag>
)

const { getByTestId, rerender } = render(<Component />)
rerender(<Component />)

const pointer = await drag(getByTestId("draggable-button")).to(100, 100)
pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(1)
expect(x.get()).toBeGreaterThanOrEqual(100)
})

test("drag gesture does not start when clicking a child button", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
<MockDrag>
<motion.div
data-testid="draggable"
drag
onDragStart={onDragStart}
style={{ x }}
>
<button data-testid="child-button">Click me</button>
</motion.div>
</MockDrag>
)

const { getByTestId, rerender } = render(<Component />)
rerender(<Component />)

const pointer = await drag(
getByTestId("draggable"),
getByTestId("child-button")
).to(100, 100)
pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(0)
expect(x.get()).toBe(0)
})

test("drag gesture starts on a motion.input with drag prop", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
<MockDrag>
<motion.input
data-testid="draggable-input"
drag
onDragStart={onDragStart}
style={{ x }}
/>
</MockDrag>
)

const { getByTestId, rerender } = render(<Component />)
rerender(<Component />)

const pointer = await drag(getByTestId("draggable-input")).to(100, 100)
pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(1)
expect(x.get()).toBeGreaterThanOrEqual(100)
})

test("drag gesture starts on a motion.a with drag prop", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
<MockDrag>
<motion.a
data-testid="draggable-link"
drag
onDragStart={onDragStart}
style={{ x }}
href="#"
/>
</MockDrag>
)

const { getByTestId, rerender } = render(<Component />)
rerender(<Component />)

const pointer = await drag(getByTestId("draggable-link")).to(100, 100)
pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(1)
expect(x.get()).toBeGreaterThanOrEqual(100)
})

test("drag gesture does not start when clicking a child input", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
<MockDrag>
<motion.div
data-testid="draggable"
drag
onDragStart={onDragStart}
style={{ x }}
>
<input data-testid="child-input" />
</motion.div>
</MockDrag>
)

const { getByTestId, rerender } = render(<Component />)
rerender(<Component />)

const pointer = await drag(
getByTestId("draggable"),
getByTestId("child-input")
).to(100, 100)
pointer.end()

await nextFrame()

expect(onDragStart).toBeCalledTimes(0)
expect(x.get()).toBe(0)
})
})