diff --git a/packages/dnb-eufemia/src/components/height-animation/HeightAnimationInstance.ts b/packages/dnb-eufemia/src/components/height-animation/HeightAnimationInstance.ts
index a5cbda2a300..3c6e77083f5 100644
--- a/packages/dnb-eufemia/src/components/height-animation/HeightAnimationInstance.ts
+++ b/packages/dnb-eufemia/src/components/height-animation/HeightAnimationInstance.ts
@@ -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')
@@ -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
@@ -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)) ||
@@ -228,6 +228,7 @@ export default class HeightAnimation {
return
}
+ this.stop()
this.isAnimating = true
// make the animation
@@ -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()
}
@@ -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'
@@ -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'
}
diff --git a/packages/dnb-eufemia/src/components/height-animation/__tests__/HeightAnimationInstance.test.ts b/packages/dnb-eufemia/src/components/height-animation/__tests__/HeightAnimationInstance.test.ts
index 4ab4cf39d6e..93a42b545a7 100644
--- a/packages/dnb-eufemia/src/components/height-animation/__tests__/HeightAnimationInstance.test.ts
+++ b/packages/dnb-eufemia/src/components/height-animation/__tests__/HeightAnimationInstance.test.ts
@@ -1,3 +1,4 @@
+import { wait } from '../../../core/jest/jestSetup'
import HeightAnimationInstance from '../HeightAnimationInstance'
import {
simulateAnimationEnd,
@@ -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)
@@ -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', () => {
@@ -146,6 +221,83 @@ 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', () => {
@@ -153,7 +305,19 @@ describe('HeightAnimationInstance', () => {
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)
@@ -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)
diff --git a/packages/dnb-eufemia/src/components/height-animation/stories/HeightAnimation.stories.tsx b/packages/dnb-eufemia/src/components/height-animation/stories/HeightAnimation.stories.tsx
index 86b6ba3eb08..e2c4926b4dd 100644
--- a/packages/dnb-eufemia/src/components/height-animation/stories/HeightAnimation.stories.tsx
+++ b/packages/dnb-eufemia/src/components/height-animation/stories/HeightAnimation.stories.tsx
@@ -87,3 +87,66 @@ const StyledSection = styled(Section)`
transform: translateY(0);
}
`
+
+export function HeightAnimationKeepInDOM() {
+ const Example = () => {
+ const [openState, setOpenState] = React.useState(true)
+ const [contentState, setContentState] = React.useState(false)
+
+ const onChangeHandler = ({ checked }) => {
+ setOpenState(checked)
+ }
+
+ // console.log('contentState', contentState)
+
+ return (
+ <>
+
+ Open/close
+
+ {
+ setContentState(checked)
+ }}
+ space={{ top: true, bottom: true }}
+ >
+ Change height inside
+
+
+
+
+
+ {contentState && More content
}
+
+
+ >
+ )
+ }
+
+ const StyledSection = styled(Section)`
+ .content-element {
+ transition: transform 1s var(--easing-default);
+ transform: translateY(-2rem);
+
+ padding: 4rem 0;
+ }
+
+ .dnb-height-animation--parallax .content-element {
+ transform: translateY(0);
+ }
+ `
+
+ return
+}
diff --git a/packages/dnb-eufemia/src/components/height-animation/useHeightAnimation.tsx b/packages/dnb-eufemia/src/components/height-animation/useHeightAnimation.tsx
index 7070c63577d..19a5ecd6cf5 100644
--- a/packages/dnb-eufemia/src/components/height-animation/useHeightAnimation.tsx
+++ b/packages/dnb-eufemia/src/components/height-animation/useHeightAnimation.tsx
@@ -154,7 +154,7 @@ export function useHeightAnimation(
const firstPaintStyle = ((open &&
!isVisible &&
!isAnimating &&
- instRef.current?.firstPaintStyle()) ||
+ instRef.current?.firstPaintStyle) ||
{}) as React.CSSProperties
const isInDOM = open || isVisible
@@ -170,6 +170,11 @@ export function useHeightAnimation(
}
function useOpenClose({ open, instRef, isInitialRenderRef, targetRef }) {
+ const isTest =
+ typeof process !== 'undefined' &&
+ process.env.NODE_ENV === 'test' &&
+ typeof globalThis.IS_TEST === 'undefined'
+
useLayoutEffect(() => {
instRef.current.setElement(targetRef.current)
@@ -182,19 +187,12 @@ function useOpenClose({ open, instRef, isInitialRenderRef, targetRef }) {
} else {
instRef.current.setAsClosed()
}
- }
- }, [open, targetRef, instRef, isInitialRenderRef])
-
- const isTest =
- typeof process !== 'undefined' &&
- process.env.NODE_ENV === 'test' &&
- typeof globalThis.IS_TEST === 'undefined'
-
- useLayoutEffect(() => {
- if (open) {
- instRef.current.open()
} else {
- instRef.current.close()
+ if (open) {
+ instRef.current.open()
+ } else {
+ instRef.current.close()
+ }
}
// For testing purposes, we need to trigger the transitionend event
@@ -202,7 +200,7 @@ function useOpenClose({ open, instRef, isInitialRenderRef, targetRef }) {
const event = new CustomEvent('transitionend')
targetRef.current?.dispatchEvent(event)
}
- }, [open, instRef, isInitialRenderRef, targetRef, isTest])
+ }, [open, targetRef, instRef, isInitialRenderRef, isTest])
useLayoutEffect(() => {
const run = () => {