-
-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: #83、exit delay #84
Changes from all commits
0767393
fc59838
ff1c05e
fe886a7
dad3a4f
7466634
eb036fe
8707b6c
7cb854a
dd33a99
792ad30
e7d541c
95f9b92
22a807d
0246eb6
d528105
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,41 +1,43 @@ | ||
import { useMotionConfig } from '@/components/motion-config/context' | ||
import type { AnimatePresenceProps } from './types' | ||
import type { MotionState } from '@/state' | ||
import { frame } from 'framer-motion/dom' | ||
|
||
export function usePopLayout(props: AnimatePresenceProps) { | ||
const styles = new WeakMap<MotionState, HTMLStyleElement>() | ||
const config = useMotionConfig() | ||
|
||
function addPopStyle(state: MotionState) { | ||
if (props.mode !== 'popLayout') | ||
return | ||
const parent = state.element.offsetParent | ||
const parentWidth | ||
= parent instanceof HTMLElement ? parent.offsetWidth || 0 : 0 | ||
const size = { | ||
height: state.element.offsetHeight || 0, | ||
width: state.element.offsetWidth || 0, | ||
top: state.element.offsetTop, | ||
left: state.element.offsetLeft, | ||
right: 0, | ||
} | ||
size.right = parentWidth - size.width - size.left | ||
const x = props.anchorX === 'left' ? `left: ${size.left}` : `right: ${size.right}` | ||
|
||
state.element.dataset.motionPopId = state.id | ||
const style = document.createElement('style') | ||
if (config.value.nonce) { | ||
style.nonce = config.value.nonce | ||
} | ||
styles.set(state, style) | ||
document.head.appendChild(style) | ||
Comment on lines
26
to
31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Creating and appending a new Recommendation: Consider using a single |
||
style.textContent = ` | ||
[data-motion-pop-id="${state.id}"] { | ||
animation: pop 0.3s ease-in-out; | ||
} | ||
` | ||
if (style.sheet) { | ||
style.sheet.insertRule(` | ||
[data-motion-pop-id="${state.id}"] { | ||
position: absolute !important; | ||
width: ${size.width}px !important; | ||
height: ${size.height}px !important; | ||
top: ${size.top}px !important; | ||
left: ${size.left}px !important; | ||
} | ||
${x}px !important; | ||
} | ||
`) | ||
Comment on lines
33
to
41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of Recommendation: Ensure all dynamic values inserted into CSS rules are properly sanitized to prevent CSS injection. Alternatively, consider safer methods to apply styles, such as setting styles directly on elements via JavaScript, which avoids the complexity and risks associated with dynamic CSS rules. |
||
} | ||
} | ||
|
@@ -45,7 +47,7 @@ export function usePopLayout(props: AnimatePresenceProps) { | |
if (!style) | ||
return | ||
styles.delete(state) | ||
requestIdleCallback(() => { | ||
frame.render(() => { | ||
document.head.removeChild(style) | ||
}) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ export * from './animate-presence' | |
export * from './motion-config' | ||
export * from './reorder' | ||
Comment on lines
3
to
4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Recommended Change: // Example for motion-config
export { specificConfig, anotherConfig } from './motion-config'
// Adjust for actual exported members |
||
export { default as RowValue } from './RowValue.vue' | ||
export { mountedStates } from '@/state' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of an absolute path ( Recommended Change: // If possible, convert to a relative path
export { mountedStates } from '../../state'
// Adjust the path as necessary based on the actual structure |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ import type { DefineComponent, ExtractPropTypes, ExtractPublicPropTypes, Intrins | |
import { defineComponent, h } from 'vue' | ||
import Motion from './Motion.vue' | ||
import type { MotionProps } from './Motion.vue' | ||
import type { MotionHTMLAttributes } from '@/types' | ||
|
||
type ComponentProps<T> = T extends DefineComponent< | ||
ExtractPropTypes<infer Props>, | ||
Comment on lines
7
to
8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The type definition for |
||
|
@@ -15,7 +16,9 @@ type MotionComponentProps = { | |
} | ||
type MotionKeys = keyof MotionComponentProps | ||
|
||
interface MotionNameSpace extends Record<keyof IntrinsicElementAttributes, DefineComponent<MotionProps<keyof IntrinsicElementAttributes, unknown>>> { | ||
type MotionNameSpace = { | ||
[K in keyof IntrinsicElementAttributes]: DefineComponent<MotionProps<K, unknown> & MotionHTMLAttributes<K>> | ||
} & { | ||
create: MotionComponentProps['create'] | ||
} | ||
Comment on lines
+19
to
23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,22 +19,22 @@ export class LayoutFeature extends Feature { | |
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
didUpdate() { | ||
if (this.state.options.layout || this.state.options.layoutId) | ||
if (this.state.options.layout || this.state.options.layoutId || this.state.options.drag) | ||
this.state.visualElement.projection?.root?.didUpdate() | ||
} | ||
|
||
mount() { | ||
const options = this.state.options | ||
const layoutGroup = this.state.options.layoutGroup | ||
if (options.layout || options.layoutId || options.drag) { | ||
if (options.layout || options.layoutId) { | ||
const projection = this.state.visualElement.projection | ||
if (projection) { | ||
projection.promote() | ||
layoutGroup?.group?.add(projection) | ||
} | ||
this.didUpdate() | ||
globalProjectionState.hasEverUpdated = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Directly modifying a global state ( |
||
} | ||
this.didUpdate() | ||
} | ||
|
||
beforeUnmount(): void { | ||
|
@@ -55,10 +55,15 @@ export class LayoutFeature extends Feature { | |
const layoutGroup = this.state.options.layoutGroup | ||
const projection = this.state.visualElement.projection | ||
|
||
if (layoutGroup?.group && projection) { | ||
layoutGroup.group.remove(projection) | ||
} | ||
else { | ||
if (projection) { | ||
if (layoutGroup?.group) { | ||
layoutGroup.group.remove(projection) | ||
} | ||
|
||
// Check lead's animation progress, if it exists, skip update to prevent lead from jumping | ||
if (projection.getStack()?.lead?.animationProgress) { | ||
return | ||
} | ||
this.didUpdate() | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export * from 'framer-motion/dom' | ||
export { addScaleCorrector } from 'framer-motion/dist/es/projection/styles/scale-correction.mjs' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The export from a deeply nested path within Suggested Change: |
||
export { motionValue as useMotionValue } from 'framer-motion/dom' | ||
export * from './components' | ||
Comment on lines
1
to
4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Suggested Change: // Instead of export * from 'module/path'
export { specificModule1, specificModule2 } from 'module/path'; |
||
export { default as LayoutGroup } from './components/LayoutGroup.vue' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,13 +40,11 @@ export function animateUpdates( | |
this.target = { ...this.baseTarget } | ||
const animationOptions: Record<string, $Transition> = {} | ||
const transition = { ...this.options.transition } | ||
Comment on lines
41
to
42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of the spread operator to clone Recommendation: |
||
|
||
// 处理直接动画或状态动画 | ||
if (directAnimate) | ||
resolveDirectAnimation.call(this, directAnimate, directTransition, animationOptions) | ||
else | ||
resolveStateAnimation.call(this, controlActiveState, animationOptions) | ||
|
||
const factories = createAnimationFactories.call(this, prevTarget, animationOptions, controlDelay) | ||
const { getChildAnimations, childAnimations } = setupChildAnimations.call(this, transition, controlActiveState, isFallback) | ||
|
||
|
@@ -120,7 +118,6 @@ function createAnimationFactories( | |
Object.keys(this.target).forEach((key: any) => { | ||
if (!hasChanged(prevTarget[key], this.target[key])) | ||
return | ||
|
||
this.baseTarget[key] ??= style.get(this.element, key) as string | ||
const keyValue = this.target[key] === 'none' ? transformResetValue[key] : this.target[key] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The handling of Recommendation: |
||
const targetTransition = animationOptions[key] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -174,38 +174,36 @@ export class MotionState { | |
// Unmount motion state and optionally unmount children | ||
// Handles unmounting in the correct order based on component tree | ||
unmount(unMountChildren = false) { | ||
/** | ||
* Unlike React, within the same update cycle, the execution order of unmount and mount depends on the component's order in the component tree. | ||
* Here we delay unmount for components with layoutId to ensure unmount executes after mount for layout animations. | ||
*/ | ||
const shouldDelay = this.options.layoutId && !mountedLayoutIds.has(this.options.layoutId) | ||
const unmount = () => { | ||
mountedStates.delete(this.element) | ||
this.featureManager.unmount() | ||
if (unMountChildren && !shouldDelay) { | ||
frame.render(() => { | ||
this.visualElement?.unmount() | ||
}) | ||
if (unMountChildren) { | ||
Array.from(this.children).reverse().forEach(this.unmountChild) | ||
} | ||
else { | ||
|
||
const unmountState = () => { | ||
this.parent?.children?.delete(this) | ||
mountedStates.delete(this.element) | ||
this.featureManager.unmount() | ||
this.visualElement?.unmount() | ||
} | ||
// Recursively unmount children in child-to-parent order | ||
if (unMountChildren) { | ||
const unmountChild = (child: MotionState) => { | ||
child.unmount(true) | ||
child.children?.forEach(unmountChild) | ||
} | ||
Array.from(this.children).forEach(unmountChild) | ||
// Delay unmount if needed for layout animations | ||
if (shouldDelay) { | ||
Promise.resolve().then(unmountState) | ||
Comment on lines
+194
to
+195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of Recommendation: Wrap the Promise.resolve().then(unmountState).catch(error => console.error('Failed to unmount state:', error)); |
||
} | ||
else { | ||
unmountState() | ||
} | ||
this.parent?.children?.delete(this) | ||
} | ||
|
||
// Delay unmount if needed for layout animations | ||
if (shouldDelay) { | ||
Promise.resolve().then(() => { | ||
unmount() | ||
}) | ||
} | ||
else { | ||
unmount() | ||
} | ||
unmount() | ||
} | ||
|
||
private unmountChild(child: MotionState) { | ||
child.unmount(true) | ||
} | ||
|
||
// Called before updating, executes in parent-to-child order | ||
|
@@ -236,7 +234,7 @@ export class MotionState { | |
}) | ||
if (isAnimate) { | ||
this.animateUpdates({ | ||
isFallback: !isActive, | ||
isFallback: !isActive && name !== 'exit' && this.visualElement.isControllingVariants, | ||
}) | ||
} | ||
} | ||
Comment on lines
234
to
240
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The recursive call to Recommendation: Consider adding error handling around the recursive call to ensure stability. For example: this.visualElement.variantChildren?.forEach((child) => {
try {
((child as any).state as MotionState).setActive(name, isActive, false);
} catch (error) {
console.error('Error updating active state for child:', error);
}
}); |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,7 +49,7 @@ export function removeItem<T>(array: T[], item: T) { | |
} | ||
|
||
export function getOptions(options: $Transition, key: string): $Transition { | ||
return options[key as any] ? { ...options, ...options[key as any] } : { ...options } | ||
return options[key as any] ? { ...options, ...options[key as any], [key]: undefined } : { ...options } | ||
Comment on lines
51
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function Recommendation:
|
||
} | ||
|
||
export function isCssVar(name: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function Recommendation:
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
custom
property is currently typed asany
, which can lead to potential issues with type safety and maintainability. It's recommended to replaceany
with a more specific type or an interface that accurately describes the expected structure ofcustom
. This change would leverage TypeScript's type checking to prevent runtime errors and improve code quality.