Skip to content

Commit

Permalink
Merge pull request #1594 from framer/fix/layout-scroll-root
Browse files Browse the repository at this point in the history
Fix layout animations in position: fixed
  • Loading branch information
mergetron[bot] committed Jun 27, 2022
2 parents 7da1247 + adae211 commit c1d68a2
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 1 deletion.
68 changes: 68 additions & 0 deletions dev/projection/single-element-page-scroll-overlay.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<html>
<head>
<style>
body {
padding: 0;
margin: 0;
}

#box {
width: 100px;
height: 100px;
background-color: #00cc88;
}

#overlay {
position: fixed;
inset: 0;
}

#trigger-overflow {
width: 1px;
height: 1px;
position: absolute;
top: 2000px;
left: 2000px;
}

[data-layout-correct="false"] {
background: #dd1144 !important;
opacity: 0.5;
}
</style>
</head>
<body>
<div id="overlay">
<div id="box"></div>
</div>
<div id="trigger-overflow"></div>

<script src="../../packages/framer-motion/dist/projection.dev.js"></script>
<script src="./script-assert.js"></script>
<script src="./script-undo.js"></script>
<script>
const { createNode } = window.Undo
const { matchViewportBox, addPageScroll } = window.Assert

const overlay = document.getElementById("overlay")
const overlayProjection = createNode(overlay, undefined, {
layoutScroll: true,
layout: false,
})

const box = document.getElementById("box")
const boxProjection = createNode(box, overlayProjection)

const boxOrigin = box.getBoundingClientRect()

boxProjection.willUpdate()

const scrollOffset = [50, 100]
window.scrollTo(...scrollOffset)

boxProjection.root.didUpdate()

matchViewportBox(box, { top: 0, left: 0, bottom: 100, right: 100 })
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const DocumentProjectionNode = createProjectionNode<Window>({
x: document.documentElement.scrollLeft || document.body.scrollLeft,
y: document.documentElement.scrollTop || document.body.scrollTop,
}),
checkIsScrollRoot: () => true,
})
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ export const HTMLProjectionNode = createProjectionNode<HTMLElement>({
resetTransform: (instance, value) => {
instance.style.transform = value ?? "none"
},
checkIsScrollRoot: (instance) =>
Boolean(window.getComputedStyle(instance).position === "fixed"),
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let rootNode: IProjectionNode

export const TestRootNode = createProjectionNode<{}>({
measureScroll: (_instance) => ({ x: 0, y: 0 }),
checkIsScrollRoot: () => true,
})

interface TestInstance {
Expand All @@ -23,6 +24,7 @@ export const TestProjectionNode = createProjectionNode<TestInstance>({
return rootNode
},
resetTransform: (instance) => instance.resetTransform?.(),
checkIsScrollRoot: () => false,
})

let id = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function createProjectionNode<I>({
attachResizeListener,
defaultParent,
measureScroll,
checkIsScrollRoot,
resetTransform,
}: ProjectionNodeConfig<I>) {
return class ProjectionNode implements IProjectionNode<I> {
Expand Down Expand Up @@ -212,6 +213,11 @@ export function createProjectionNode<I>({
*/
scroll?: Point

/**
* If this element is a scroll root, we ignore scrolls up the tree.
*/
isScrollRoot?: boolean

/**
* Flag to true if we think this layout has been changed. We can't always know this,
* currently we set it to true every time a component renders, or if it has a layoutDependency
Expand Down Expand Up @@ -718,6 +724,7 @@ export function createProjectionNode<I>({

updateScroll() {
if (this.options.layoutScroll && this.instance) {
this.isScrollRoot = checkIsScrollRoot(this.instance)
this.scroll = measureScroll(this.instance)
}
}
Expand Down Expand Up @@ -776,9 +783,26 @@ export function createProjectionNode<I>({
*/
for (let i = 0; i < this.path.length; i++) {
const node = this.path[i]
const { scroll, options } = node
const { scroll, options, isScrollRoot } = node

if (node !== this.root && scroll && options.layoutScroll) {
/**
* If this is a new scroll root, we want to remove all previous scrolls
* from the viewport box.
*/
if (isScrollRoot) {
copyBoxInto(boxWithoutScroll, box)
const { scroll: rootScroll } = this.root
/**
* Undo the application of page scroll that was originally added
* to the measured bounding box.
*/
if (rootScroll) {
translateAxis(boxWithoutScroll.x, -rootScroll.x)
translateAxis(boxWithoutScroll.y, -rootScroll.y)
}
}

translateAxis(boxWithoutScroll.x, scroll.x)
translateAxis(boxWithoutScroll.y, scroll.y)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/framer-motion/src/projection/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface IProjectionNode<I = unknown> {
targetDelta?: Delta
targetWithTransforms?: Box
scroll?: Point
isScrollRoot?: boolean
treeScale?: Point
projectionDelta?: Delta
latestValues: ResolvedValues
Expand Down Expand Up @@ -141,6 +142,7 @@ export interface ProjectionNodeConfig<I> {
notifyResize: VoidFunction
) => VoidFunction
measureScroll: (instance: I) => Point
checkIsScrollRoot: (instance: I) => boolean
resetTransform?: (instance: I, value?: string) => void
}

Expand Down

0 comments on commit c1d68a2

Please sign in to comment.