Skip to content
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(HeightAnimation): enhance calculation of height #3335

Merged
merged 1 commit into from
Feb 28, 2024
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 @@ -44,6 +44,12 @@ export default class HeightAnimation {
isAnimating: boolean
__currentHeight: number

firstPaintStyle = {
visibility: 'hidden',
opacity: '0', // prevents before/after elements to be visible
height: 'auto',
}

constructor(opts: HeightAnimationOptions = {}) {
this.isInBrowser = typeof window !== 'undefined'
this.setState('init')
Expand Down Expand Up @@ -164,14 +170,6 @@ export default class HeightAnimation {
getHeight() {
return parseFloat(String(this.elem?.clientHeight)) || null
}
firstPaintStyle() {
return {
position: 'absolute',
visibility: 'hidden',
opacity: '0', // prevents before/after elements to be visible
height: 'auto',
}
}
getUnknownHeight() {
if (!this.elem) {
return null
Expand All @@ -181,13 +179,15 @@ export default class HeightAnimation {
return this.__currentHeight
}

const width = this.elem.clientWidth
const clonedElem = this.elem.cloneNode(true) as HTMLElement
this.elem.parentNode?.insertBefore(clonedElem, this.elem.nextSibling)

for (const key in this.firstPaintStyle) {
clonedElem.style[key] = this.firstPaintStyle[key]
}
clonedElem.style.height = 'auto'
clonedElem.style.width = width ? `${String(width)}px` : 'auto' // set width because of the "position: absolute"
clonedElem.style.position = 'absolute' // not a part of the "firstPaintStyle"

const height =
parseFloat(String(clonedElem.clientHeight)) ||
Expand Down Expand Up @@ -228,6 +228,7 @@ export default class HeightAnimation {
return
}

this.stop()
this.isAnimating = true

// make the animation
Expand Down Expand Up @@ -275,7 +276,7 @@ export default class HeightAnimation {
const toHeight = this.getUnknownHeight()

this.addEndEvent((e) => {
if (e.target === e.currentTarget) {
if (e.target === e.currentTarget || !e.currentTarget) {
this.setState('opened')
this.readjust()
}
Expand All @@ -299,7 +300,7 @@ export default class HeightAnimation {
const fromHeight = this.getHeight()

this.addEndEvent((e) => {
if (e.target === e.currentTarget) {
if (e.target === e.currentTarget || !e.currentTarget) {
if (this.elem) {
this.elem.style.visibility = 'hidden'
this.elem.style.overflowY = 'clip'
Expand Down Expand Up @@ -342,7 +343,10 @@ export default class HeightAnimation {
this.callAnimationStart()

this.addEndEvent((e) => {
if (this.state === 'adjusting' && e.target === e.currentTarget) {
if (
this.state === 'adjusting' &&
(e.target === e.currentTarget || !e.currentTarget)
) {
if (this.elem) {
this.elem.style.height = 'auto'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { wait } from '../../../core/jest/jestSetup'
import HeightAnimationInstance from '../HeightAnimationInstance'
import {
simulateAnimationEnd,
Expand Down Expand Up @@ -37,6 +38,20 @@ describe('HeightAnimationInstance', () => {
expect(inst.elem).toBeUndefined()
})

it('firstPaintStyle should have these properties', () => {
const inst = new HeightAnimationInstance()
expect(inst.firstPaintStyle).toEqual({
height: 'auto',
opacity: '0',
visibility: 'hidden',
})
expect(inst.firstPaintStyle).not.toEqual(
expect.objectContaining({
position: 'absolute',
})
)
})

it('getHeight should return height', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)
Expand All @@ -46,55 +61,115 @@ describe('HeightAnimationInstance', () => {
expect(inst.getHeight()).toBe(100)
})

it('getUnknownHeight should return proper height', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)
describe('getUnknownHeight', () => {
it('should return proper height', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

mockHeight(100, element)
mockHeight(100, element)

expect(inst.getUnknownHeight()).toBe(100)
})
expect(inst.getUnknownHeight()).toBe(100)
})

it('open should call getUnknownHeight', () => {
const inst = new HeightAnimationInstance()
it('should create a cloned element', async () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

jest.spyOn(inst, 'getUnknownHeight').mockImplementation(jest.fn())
mockHeight(100, element)

inst.setElement(element)
inst.setState('closed')
inst.open()
const addedNodes = []
const removedNodes = []

const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
if (mutation.removedNodes?.length) {
removedNodes.push(mutation.removedNodes)
}
if (mutation.addedNodes?.length) {
addedNodes.push(mutation.addedNodes)
}
}
}
})

expect(inst.getUnknownHeight).toHaveBeenCalledTimes(1)
})
observer.observe(document.body, {
childList: true,
})

it('getUnknownHeight should use cached height during animation', () => {
const inst = new HeightAnimationInstance()
inst.getUnknownHeight()

mockHeight(100, element)
await wait(1)

expect(inst.__currentHeight).toBe(undefined)
expect(inst.isAnimating).toBe(undefined)
observer.disconnect()

inst.setElement(element)
inst.setState('closed')
inst.open()
expect(addedNodes).toHaveLength(1)
expect(removedNodes).toHaveLength(1)
})

expect(inst.isAnimating).toBe(true)
expect(inst.__currentHeight).toBe(100)
it('should create a cloned element with firstPaintStyle styles', async () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

mockHeight(200, element)
mockHeight(100, element)

inst.getUnknownHeight()
const styles = []

expect(inst.__currentHeight).toBe(100)
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
if (mutation.addedNodes?.length) {
styles.push(mutation.addedNodes[0])
}
}
}
})

observer.observe(document.body, {
childList: true,
})

inst.getUnknownHeight()

await wait(1)

observer.disconnect()

expect(styles).toHaveLength(1)
expect(styles[0].getAttribute('style')).toBe(
'visibility: hidden; opacity: 0; height: auto; width: auto; position: absolute;'
)
})

delete inst.elem
it('should use cached height during animation', () => {
const inst = new HeightAnimationInstance()

expect(inst.getUnknownHeight()).toBe(null)
mockHeight(100, element)

inst.callAnimationEnd()
expect(inst.__currentHeight).toBe(undefined)
expect(inst.isAnimating).toBe(undefined)

expect(inst.__currentHeight).toBe(undefined)
inst.setElement(element)
inst.setState('closed')
inst.open()

expect(inst.isAnimating).toBe(true)
expect(inst.__currentHeight).toBe(100)

mockHeight(200, element)

inst.getUnknownHeight()

expect(inst.__currentHeight).toBe(100)

delete inst.elem

expect(inst.getUnknownHeight()).toBe(null)

inst.callAnimationEnd()

expect(inst.__currentHeight).toBe(undefined)
})
})

describe('start', () => {
Expand Down Expand Up @@ -146,14 +221,103 @@ describe('HeightAnimationInstance', () => {
expect(onStart).toHaveBeenCalledTimes(0)
expect(onEnd).toHaveBeenCalledTimes(0)
})

it('should call stop', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

jest.spyOn(inst, 'stop').mockImplementation(jest.fn())
inst.start(100, 200)

expect(inst.stop).toHaveBeenCalledTimes(1)
})

it('should set reqId1 and reqId2', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

inst.start(100, 200)

expect(inst.reqId1).toBe(1)
expect(inst.reqId2).toBeUndefined()

nextAnimationFrame()

expect(inst.reqId1).toBe(1)
expect(inst.reqId2).toBe(1)
})

it('should set height style', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

inst.start(100, 200)

expect(inst.elem.style.height).toBe('')

nextAnimationFrame()

expect(inst.elem.style.height).toBe('100px')

nextAnimationFrame()

expect(inst.elem.style.height).toBe('200px')
})

it('should set not height style when element is missing', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)
const elem = inst.elem

inst.start(100, 200)

expect(elem.style.height).toBe('')

nextAnimationFrame()

expect(elem.style.height).toBe('100px')

inst.elem = undefined // here we remove the element during the second animation frame
nextAnimationFrame()

expect(elem.style.height).toBe('100px')
})

it('should not run when element is not set', () => {
const inst = new HeightAnimationInstance()
inst.start(100, 200)
expect(inst.reqId1).toBeUndefined()
expect(inst.reqId2).toBeUndefined()
})

it('should set isAnimating to true', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

inst.start(100, 200)

expect(inst.isAnimating).toBe(true)
})
})

describe('open', () => {
beforeEach(() => {
globalThis.bypassTime = 1
})

it('should call setAsOpen when criterias are met', () => {
it('should call getUnknownHeight', () => {
const inst = new HeightAnimationInstance()

jest.spyOn(inst, 'getUnknownHeight').mockImplementation(jest.fn())

inst.setElement(element)
inst.setState('closed')
inst.open()

expect(inst.getUnknownHeight).toHaveBeenCalledTimes(1)
})

it('should call setAsOpen when criteria are met', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

Expand Down Expand Up @@ -256,7 +420,7 @@ describe('HeightAnimationInstance', () => {
globalThis.bypassTime = 1
})

it('should call setAsClosed when criterias are met', () => {
it('should call setAsClosed when criteria are met', () => {
const inst = new HeightAnimationInstance()
inst.setElement(element)

Expand Down
Loading
Loading