Skip to content

Commit dbd3081

Browse files
committed
fix: make reducedMotion reactive to user preferences
1 parent e1745e6 commit dbd3081

File tree

8 files changed

+83
-70
lines changed

8 files changed

+83
-70
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { mount } from '@vue/test-utils'
3+
import { motionValue } from 'framer-motion/dom'
4+
import { defineComponent, nextTick } from 'vue'
5+
import MotionConfig from '@/components/motion-config/MotionConfig.vue'
6+
import { Motion } from '@/components/motion'
7+
8+
describe('reducedMotion', () => {
9+
it('reducedMotion always', async () => {
10+
const scale = motionValue(1)
11+
const wrapper = mount(defineComponent({
12+
setup() {
13+
return () => (
14+
<MotionConfig reducedMotion="always">
15+
<Motion animate={{ scale: 0.5 }} style={{ scale }} />
16+
</MotionConfig>
17+
)
18+
},
19+
}))
20+
await nextTick()
21+
await new Promise(resolve => setTimeout(resolve, 20))
22+
expect(scale.get()).toBe(0.5)
23+
})
24+
})

packages/motion/src/components/motion/use-motion-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function useMotionState(props: MotionProps) {
3333
*/
3434
if (
3535
process.env.NODE_ENV !== 'production'
36+
// @ts-expect-error
3637
&& props.features?.length
3738
&& lazyMotionContext.strict
3839
) {

packages/motion/src/features/animation/animation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { isDef } from '@vueuse/core'
1212
import type { VisualElement } from 'framer-motion'
1313
import { animate, noop } from 'framer-motion/dom'
1414
import { createVisualElement } from '@/state/create-visual-element'
15+
import { prefersReducedMotion } from 'framer-motion/dist/es/utils/reduced-motion/state.mjs'
1516

1617
const STATE_TYPES = ['initial', 'animate', 'whileInView', 'whileHover', 'whilePress', 'whileDrag', 'whileFocus', 'exit'] as const
1718
export type StateType = typeof STATE_TYPES[number]
@@ -42,6 +43,7 @@ export class AnimationFeature extends Feature {
4243
},
4344
reducedMotionConfig: this.state.options.motionConfig.reducedMotion,
4445
})
46+
4547
this.state.animateUpdates = this.animateUpdates
4648
if (this.state.isMounted())
4749
this.state.startAnimation()
@@ -62,6 +64,10 @@ export class AnimationFeature extends Feature {
6264
isFallback,
6365
isExit,
6466
} = {}) => {
67+
// check if the user has reduced motion
68+
const { reducedMotion } = this.state.options.motionConfig
69+
this.state.visualElement.shouldReduceMotion = reducedMotion === 'always' || (reducedMotion === 'user' && !!prefersReducedMotion.current)
70+
6571
const prevTarget = this.state.target
6672
this.state.target = { ...this.state.baseTarget }
6773
let animationOptions: $Transition = {}

packages/motion/src/framer-motion.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,8 @@ declare module 'framer-motion/dist/es/render/utils/setters.mjs' {
6767

6868
export const setTarget: (visualElement: VisualElement, definition: any) => void
6969
}
70+
71+
declare module 'framer-motion/dist/es/utils/reduced-motion/state.mjs' {
72+
export const prefersReducedMotion: { current: boolean }
73+
export const hasReducedMotionListener: { current: boolean }
74+
}

packages/motion/vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export default defineConfig({
3232
'framer-motion/dist/es/render/svg/SVGVisualElement.mjs': path.resolve(__dirname, 'node_modules/framer-motion/dist/es/render/svg/SVGVisualElement.mjs'),
3333
'framer-motion/dist/es/animation/interfaces/motion-value.mjs': path.resolve(__dirname, 'node_modules/framer-motion/dist/es/animation/interfaces/motion-value.mjs'),
3434
'framer-motion/dist/es/render/utils/setters.mjs': path.resolve(__dirname, 'node_modules/framer-motion/dist/es/render/utils/setters.mjs'),
35+
'framer-motion/dist/es/utils/reduced-motion/state.mjs': path.resolve(__dirname, 'node_modules/framer-motion/dist/es/utils/reduced-motion/state.mjs'),
36+
3537
},
3638
},
3739
build: {

packages/motion/vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export default defineConfig({
2525
'framer-motion/dist/es/render/html/HTMLVisualElement.mjs': path.resolve(__dirname, 'node_modules/framer-motion/dist/es/render/html/HTMLVisualElement.mjs'),
2626
'framer-motion/dist/es/render/svg/SVGVisualElement.mjs': path.resolve(__dirname, 'node_modules/framer-motion/dist/es/render/svg/SVGVisualElement.mjs'),
2727
'framer-motion/dist/es/animation/interfaces/motion-value.mjs': path.resolve(__dirname, 'node_modules/framer-motion/dist/es/animation/interfaces/motion-value.mjs'),
28+
'framer-motion/dist/es/utils/reduced-motion/state.mjs': path.resolve(__dirname, 'node_modules/framer-motion/dist/es/utils/reduced-motion/state.mjs'),
2829
},
2930
},
3031
})

playground/nuxt/pages/test.vue

Lines changed: 10 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,22 @@
11
<script setup lang="tsx">
22
/** @jsxImportSource vue */
3-
import { AnimateNumber } from 'motion-plus-vue'
4-
import { motion } from 'motion-v'
53
import { ref } from 'vue'
64
7-
const isCompact = ref(false)
8-
const isCurrency = ref(false)
9-
10-
function Switch({ isOn, toggle }: { isOn: boolean, toggle: () => void }) {
11-
return (
12-
<motion.button
13-
class="switch-container"
14-
style={{
15-
justifyContent: `flex-${isOn ? 'end' : 'start'}`,
16-
}}
17-
initial={false}
18-
animate={{
19-
backgroundColor: isOn
20-
? 'var(--hue-6-transparent)'
21-
: '#586d8c33',
22-
}}
23-
onClick={toggle}
24-
focus={{
25-
outline: '2px solid #4ff0b7',
26-
}}
27-
>
28-
<motion.div
29-
class="switch-handle"
30-
layout
31-
data-is-on={isOn}
32-
transition={{
33-
type: 'spring',
34-
visualDuration: 0.2,
35-
bounce: 0.2,
36-
}}
37-
/>
38-
</motion.button>
39-
)
40-
}
5+
const isShow = ref(false)
416
</script>
427

438
<template>
44-
<div class="container">
45-
<AnimateNumber
46-
:format="{
47-
notation: isCompact ? 'compact' : undefined,
48-
compactDisplay: isCompact ? 'short' : undefined,
49-
roundingMode: isCompact ? 'trunc' : undefined,
50-
style: isCurrency ? 'currency' : undefined,
51-
currency: isCurrency ? 'USD' : undefined,
52-
}"
53-
locales="en-US"
54-
class="number"
55-
:transition="{
56-
visualDuration: 0.6,
57-
type: 'spring',
58-
bounce: 0.25,
59-
opacity: { duration: 0.3, ease: 'linear' },
9+
<button @click="isShow = !isShow">
10+
show
11+
</button>
12+
<MotionConfig reduced-motion="user">
13+
<Motion
14+
class="w-[100px] h-[100px] bg-red-500"
15+
:animate="{
16+
scale: isShow ? 1 : 0.5,
6017
}"
61-
:value="123.49"
6218
/>
63-
<div class="controls">
64-
<div>
65-
Currency:
66-
<Switch
67-
:is-on="isCurrency"
68-
:toggle="() => isCurrency = !isCurrency"
69-
/>
70-
</div>
71-
<div>
72-
Compact:
73-
<Switch
74-
:is-on="isCompact"
75-
:toggle="() => isCompact = !isCompact"
76-
/>
77-
</div>
78-
</div>
79-
</div>
19+
</MotionConfig>
8020
</template>
8121

8222
<style>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import { MotionConfig, motion } from '@/components'
4+
5+
const show = ref(true)
6+
const toggle = () => (show.value = !show.value)
7+
const variants = {
8+
initial: { y: 20, opacity: 0 },
9+
visible: { y: 0, opacity: 1 },
10+
hidden: { y: 0, opacity: 0.25 },
11+
}
12+
</script>
13+
14+
<template>
15+
<MotionConfig reduced-motion="always">
16+
<div style="padding: 40px">
17+
<button
18+
data-testid="toggle-btn"
19+
@click="toggle"
20+
>
21+
Toggle
22+
</button>
23+
<motion.button
24+
data-testid="motion-btn"
25+
initial="initial"
26+
:variants="variants"
27+
:while-in-view="show ? 'visible' : 'hidden'"
28+
style="width: 120px; height: 40px; margin-top: 20px"
29+
>
30+
CLICK ME
31+
</motion.button>
32+
</div>
33+
</MotionConfig>
34+
</template>

0 commit comments

Comments
 (0)