Skip to content

Commit

Permalink
feat(HeightAnimation): adjust height with animation when content changes
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker committed Sep 22, 2022
1 parent bf3769f commit 7d4644a
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,48 @@ export function HeightAnimationDefault() {
/* jsx */ `
const Example = () => {
const [openState, setOpenState] = React.useState(false)
const [isOpen, setIsOpen] = React.useState(openState)
const [contentState, setContentState] = React.useState(false)
const onChangeHandler = ({ checked }) => {
setOpenState(checked)
}
return (
<>
<ToggleButton checked={openState} onChange={onChangeHandler} bottom>
Toggle me
<ToggleButton
checked={openState}
onChange={({ checked }) => {
setOpenState(checked)
}}
right
>
Open/close
</ToggleButton>
<ToggleButton
checked={contentState || !openState}
disabled={!isOpen}
onChange={({ checked }) => {
setContentState(checked)
}}
space={{ top: true, bottom: true }}
>
Change height inside
</ToggleButton>
<Section style_type="lavender">
<Section style_type="lavender" top>
<HeightAnimation
open={openState}
onOpen={setIsOpen}
>
<P className="content-element" top="large" bottom="large">
Your content
</P>
<Section spacing style_type="lavender">
<P space={0}>Your content</P>
</Section>
{contentState && <P space={0}>More content</P>}
</HeightAnimation>
</Section>
<P top>Look at me 👀</P>
</>
)
}
Expand All @@ -49,27 +71,47 @@ export function HeightAnimationKeepInDOM() {
{
/* jsx */ `
const Example = () => {
const [openState, setOpenState] = React.useState(false)
const [openState, setOpenState] = React.useState(true)
const [isOpen, setIsOpen] = React.useState(openState)
const [contentState, setContentState] = React.useState(false)
const onChangeHandler = ({ checked }) => {
setOpenState(checked)
}
return (
<>
<ToggleButton checked={openState} onChange={onChangeHandler} bottom>
Toggle me
<ToggleButton
checked={openState}
onChange={({ checked }) => {
setOpenState(checked)
}}
right
>
Open/close
</ToggleButton>
<ToggleButton
checked={contentState || !openState}
disabled={!isOpen}
onChange={({ checked }) => {
setContentState(checked)
}}
space={{ top: true, bottom: true }}
>
Change height inside
</ToggleButton>
<StyledSection style_type="lavender">
<StyledSection style_type="lavender" top>
<HeightAnimation
open={openState}
keepInDOM={true}
duration={1000}
onOpen={setIsOpen}
>
<P className="content-element" space={0}>
Your content
</P>
<Section spacing style_type="lavender">
<P space={0}>Your content</P>
</Section>
{contentState && <P space={0}>More content</P>}
</HeightAnimation>
</StyledSection>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ showTabs: true

## Events

No events are supported at the moment.
| Events | Description |
| -------- | ----------------------------------------------------------------------------------------------------- |
| `onOpen` | _(optional)_ Is called when fully opened or closed. Returns `true` or `false` depending on the state. |
| `onOpen` | _(optional)_ Is called when fully opened or closed. Returns `true` or `false` depending on the state. |
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ showTabs: true

## Description

The HeightAnimation component is a helper component to animate from 0px to height:auto powered by CSS. It calculates the height on the fly.
The HeightAnimation component is a helper component to animate from `0` to `height: auto` powered by CSS. It calculates the height on the fly.

When the animation is done, it sets the element's height to `auto`.

The component can be used as an opt-int replacement instead of vanilla HTML Elements.

The element animation is done with a CSS transition and a `400ms` duration:
The element animation is done with a CSS transition with `400ms` in duration.

It also re-calculates and changes the height, when the given content changes.

## Accessibility

It is important to never animate from 0 to e.g. 64px – because the content may differ based on the viewport width (screen size), the content itself, or the user may even have a larger `font-size`.
It is important to never animate from 0 to e.g. 64px – because;

- the content may differ based on the viewport width (screen size)
- the content itself may change
- the user may have a larger `font-size`
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ export type HeightAnimationProps = {
*/
innerRef?: React.RefObject<HTMLElement>

/**
* Is called when fully opened or closed
* Default: null
*/
onOpen?: (isOpen: boolean) => void

/**
* Is called when animation is done and the full height has reached
* Default: null
*/
onAnimationEnd?: () => void

className?: React.ReactNode
children?: React.ReactNode | HTMLElement
}
Expand All @@ -54,17 +66,20 @@ export default function HeightAnimation({
className,
innerRef,
children,
onOpen = null,
onAnimationEnd = null,
...props
}: HeightAnimationProps & ISpacingProps) {
const ref = React.useRef<HTMLElement>()

const { isInDOM, isVisible, isVisibleParallax } = useHeightAnimation(
innerRef || ref,
{
const { isInDOM, isVisible, isVisibleParallax, isAnimating } =
useHeightAnimation(innerRef || ref, {
open,
animate,
}
)
children,
onOpen,
onAnimationEnd,
})

if (!isInDOM && !keepInDOM) {
return null
Expand All @@ -84,6 +99,7 @@ export default function HeightAnimation({
isInDOM && 'dnb-height-animation--is-in-dom',
isVisible && 'dnb-height-animation--is-visible',
isVisibleParallax && 'dnb-height-animation--parallax',
isAnimating && 'dnb-height-animation--animating',
className
)}
style={style}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('HeightAnimation', () => {
open = false,
animate = true,
element = 'div',
children,
...props
}: Partial<HeightAnimationProps>) => {
const [openState, setOpenState] = React.useState(open)
Expand All @@ -58,7 +59,7 @@ describe('HeightAnimation', () => {
animate={animate} // Optional
{...props}
>
<p className="content-element">Your content</p>
<p className="content-element">Your content {children}</p>
</HeightAnimation>
</section>
</>
Expand Down Expand Up @@ -115,6 +116,50 @@ describe('HeightAnimation', () => {
})
})

it('should adjust height when content changes', async () => {
const { rerender } = render(<Component />)

expect(document.querySelector('.dnb-height-animation')).toBeFalsy()

rerender(<Component open />)

await act(async () => {
const element = document.querySelector('.dnb-height-animation')

simulateAnimationEnd()

expect(
document
.querySelector('.dnb-height-animation')
.getAttribute('style')
).toBe('height: auto;')

rerender(<Component open>123</Component>)

await wait(1)

expect(
document
.querySelector('.dnb-height-animation')
.getAttribute('style')
).toBe('height: 0px;')

jest
.spyOn(element, 'clientHeight', 'get')
.mockImplementationOnce(() => 100)

rerender(<Component open>456</Component>)

await wait(1)

expect(
document
.querySelector('.dnb-height-animation')
.getAttribute('style')
).toBe('height: 100px;')
})
})

it('should have content in DOM when keepInDOM is true', async () => {
const { rerender } = render(<Component keepInDOM />)

Expand Down Expand Up @@ -178,6 +223,7 @@ describe('HeightAnimation', () => {
'dnb-height-animation--is-in-dom',
'dnb-height-animation--is-visible',
'dnb-height-animation--parallax',
'dnb-height-animation--animating',
])

fireEvent.click(document.querySelector('button'))
Expand All @@ -187,6 +233,7 @@ describe('HeightAnimation', () => {
'dnb-height-animation',
'dnb-height-animation--is-in-dom',
'dnb-height-animation--is-visible',
'dnb-height-animation--animating',
])

simulateAnimationEnd()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,67 @@ import styled from '@emotion/styled'
import React from 'react'
import { P } from '../../../elements'
import Section from '../../section/Section'
import ToggleButton from '../../toggle-button/ToggleButton'
import { ToggleButton, Button } from '../../'
import HeightAnimation from '../HeightAnimation'

export default {
title: 'Eufemia/Components/HeightAnimation',
}

export const HeightAnimationSandbox = () => {
const [openState, setOpenState] = React.useState(false)

const onChangeHandler = ({ checked }) => {
setOpenState(checked)
}
const [count, setCount] = React.useState(0)
const [openState, setOpenState] = React.useState(true)
const [isOpen, setIsOpen] = React.useState(true)
const [contentState, setContentState] = React.useState(false)

return (
<>
<ToggleButton checked={openState} onChange={onChangeHandler}>
Toggle me
<ToggleButton
checked={openState}
onChange={({ checked }) => {
setOpenState(checked)
}}
right
>
Open/close
</ToggleButton>

<ToggleButton
disabled={!isOpen}
checked={contentState}
onChange={({ checked }) => {
setContentState(checked)
}}
right
>
Change height inside
</ToggleButton>

<StyledSection style_type="lavender">
<Button
onClick={() => {
setCount(count + 1)
}}
>
{count}
</Button>

<StyledSection style_type="lavender" top>
<HeightAnimation
open={openState}
element="div" // Optional
animate={true} // Optional
keepInDOM={true} // Optional
duration={1000}
onOpen={setIsOpen}
>
<P className="content-element" space={0}>
Your content
</P>
<Section spacing style_type="lavender">
<P>Your content</P>
</Section>
{contentState && <P>More content</P>}
</HeightAnimation>
</StyledSection>

<P top>Look at me 👀</P>
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*/

.dnb-height-animation {
overflow: hidden;
transition: height var(--duration, 400ms) var(--easing-default);

&--animating {
overflow: hidden;
}
}
Loading

0 comments on commit 7d4644a

Please sign in to comment.