Skip to content

Commit

Permalink
feat(Forms): add onAnimationEnd property to Form.Visibility (#4356)
Browse files Browse the repository at this point in the history
I think we should use `onVisible` for both the animated and non-animated
state change. Here is the related PR #4350

While `onAnimationEnd` should be used to determine if the animated has
"ended". Pretty much like HeightAnimation works.

---------

Co-authored-by: Anders <[email protected]>
  • Loading branch information
tujoworker and langz authored Dec 4, 2024
1 parent cd47de8 commit 87728b4
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ tabs:
key: '/demos'
- title: Properties
key: '/properties'
- title: Events
key: '/events'
breadcrumb:
- text: Forms
href: /uilib/extensions/forms/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
showTabs: true
---

import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
import { VisibilityEvents } from '@dnb/eufemia/src/extensions/forms/Form/Visibility/VisibilityDocs'

## Events

<PropertiesTable props={VisibilityEvents} />
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ export const HeightAnimationEvents: PropertiesTableProps = {
status: 'optional',
},
onAnimationStart: {
doc: 'Is called when animation has started.',
doc: 'Is called when animation has started. The first parameter is a string. Depending on the state, the value can be `opening`, `closing` or `adjusting`.',
type: 'function',
status: 'optional',
},
onAnimationEnd: {
doc: 'Is called when animation is done and the full height is reached.',
doc: 'Is called when animation is done and the full height is reached. The first parameter is a string. Depending on the state, the value can be `opened`, `closed` or `adjusted`.',
type: 'function',
status: 'optional',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import React, { AriaAttributes } from 'react'
import React, { AriaAttributes, useCallback } from 'react'

import { warn } from '../../../../shared/helpers'
import useMountEffect from '../../../../shared/helpers/useMountEffect'
import useMounted from '../../../../shared/helpers/useMounted'
import HeightAnimation, {
HeightAnimationProps,
HeightAnimationAllProps,
} from '../../../../components/HeightAnimation'
import FieldProvider from '../../Field/Provider'
import useVisibility from './useVisibility'
import VisibilityContext from './VisibilityContext'

import type { Path, UseFieldProps } from '../../types'
import type { DataAttributes } from '../../hooks/useFieldProps'
import { FilterData } from '../../DataContext'
import VisibilityContext from './VisibilityContext'

export type VisibleWhen =
| {
Expand Down Expand Up @@ -76,13 +77,15 @@ export type Props = {
animate?: boolean
/** Keep the content in the DOM, even if it's not visible */
keepInDOM?: boolean
/** Callback when the content is visible. Only for when `animate` is true. */
onVisible?: HeightAnimationProps['onOpen']
/** Callback for when the content gets visible. */
onVisible?: HeightAnimationAllProps['onOpen']
/** Callback for when animation has ended */
onAnimationEnd?: HeightAnimationAllProps['onAnimationEnd']
/** To compensate for CSS gap between the rows, so animation does not jump during the animation. Provide a CSS unit or `auto`. Defaults to `null`. */
compensateForGap?: HeightAnimationProps['compensateForGap']
compensateForGap?: HeightAnimationAllProps['compensateForGap']
/** When visibility is hidden, and `keepInDOM` is true, pass these props to the children */
fieldPropsWhenHidden?: UseFieldProps & DataAttributes & AriaAttributes
element?: HeightAnimationProps['element']
element?: HeightAnimationAllProps['element']
children: React.ReactNode

/** @deprecated Use `visibleWhen` instead */
Expand All @@ -106,6 +109,7 @@ function Visibility({
inferData,
filterData,
onVisible,
onAnimationEnd,
animate,
keepInDOM,
compensateForGap,
Expand Down Expand Up @@ -144,14 +148,25 @@ function Visibility({
{children}
</VisibilityContext.Provider>
)
const mountedRef = useMounted()

const onOpen: HeightAnimationAllProps['onOpen'] = useCallback(
(state) => {
if (mountedRef.current) {
onVisible?.(state)
}
},
[mountedRef, onVisible]
)

if (animate) {
const props = !open ? fieldPropsWhenHidden : null

return (
<HeightAnimation
open={open}
onOpen={onVisible}
onAnimationEnd={onAnimationEnd}
onOpen={onOpen}
keepInDOM={Boolean(keepInDOM)}
className="dnb-forms-visibility"
compensateForGap={compensateForGap}
Expand All @@ -162,6 +177,10 @@ function Visibility({
)
}

if (mountedRef.current) {
onVisible?.(open)
}

if (keepInDOM) {
const props = !open ? fieldPropsWhenHidden : null
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PropertiesTableProps } from '../../../../shared/types'
import { HeightAnimationEvents } from '../../../../components/height-animation/HeightAnimationDocs'

export const VisibilityProperties: PropertiesTableProps = {
visibleWhen: {
Expand Down Expand Up @@ -61,11 +62,6 @@ export const VisibilityProperties: PropertiesTableProps = {
type: 'boolean',
status: 'optional',
},
onVisible: {
doc: 'Callback when the content is visible. Only for when `animate` is true.',
type: 'function',
status: 'optional',
},
compensateForGap: {
doc: 'To compensate for CSS gap between the rows, so animation does not jump during the animation. Provide a CSS unit or `auto`. Defaults to `null`.',
type: 'string',
Expand All @@ -92,3 +88,12 @@ export const VisibilityProperties: PropertiesTableProps = {
status: 'required',
},
}

export const VisibilityEvents: PropertiesTableProps = {
onVisible: {
doc: 'Callback for when the content gets visible. Returns a boolean as the first parameter.',
type: HeightAnimationEvents.onOpen.type,
status: 'optional',
},
onAnimationEnd: HeightAnimationEvents.onAnimationEnd,
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FilterData, Provider } from '../../../DataContext'
import Visibility from '../Visibility'
Expand Down Expand Up @@ -437,50 +437,155 @@ describe('Visibility', () => {
`)
})

describe('animate', () => {
it('should have "height-animation" wrapper when animate is true', async () => {
it('should have "height-animation" wrapper when animate is true', async () => {
render(
<Provider data={{ myPath: 'checked' }}>
<Visibility
visibleWhen={{
path: '/myPath',
hasValue: 'checked',
}}
animate
>
Child
</Visibility>
</Provider>
)

const element = document.querySelector('.dnb-height-animation')

expect(element).toBeInTheDocument()
expect(element).toHaveClass(
'dnb-space dnb-height-animation dnb-height-animation--is-in-dom dnb-height-animation--parallax'
)
})

describe('events', () => {
it('should not call onVisible initially', async () => {
const onVisible = jest.fn()

render(
<Provider data={{ myPath: 'checked' }}>
<Visibility
visibleWhen={{
path: '/myPath',
hasValue: 'checked',
}}
animate
>
Child
</Visibility>
</Provider>
<Form.Handler
defaultData={{
toggleValue: false,
}}
>
<Field.Boolean path="/toggleValue" />
<Form.Visibility pathTrue="/toggleValue" onVisible={onVisible}>
content
</Form.Visibility>
</Form.Handler>
)

const element = document.querySelector('.dnb-height-animation')
const checkbox = document.querySelector('input[type="checkbox"]')

expect(element).toBeInTheDocument()
expect(element).toHaveClass(
'dnb-space dnb-height-animation dnb-height-animation--is-in-dom dnb-height-animation--parallax'
)
expect(onVisible).toHaveBeenCalledTimes(0)

await userEvent.click(checkbox)

expect(onVisible).toHaveBeenCalledTimes(1)
expect(onVisible).toHaveBeenLastCalledWith(true)
})

it('should call onVisible when animation is done', async () => {
it('should call onVisible when visible again', async () => {
const onVisible = jest.fn()

const { rerender } = render(
<Visibility visible={false} onVisible={onVisible} animate>
Child
</Visibility>
render(
<Form.Handler
defaultData={{
toggleValue: false,
}}
>
<Field.Boolean path="/toggleValue" />
<Form.Visibility pathTrue="/toggleValue" onVisible={onVisible}>
content
</Form.Visibility>
</Form.Handler>
)

const checkbox = document.querySelector('input[type="checkbox"]')

expect(onVisible).toHaveBeenCalledTimes(0)

await userEvent.click(checkbox)
expect(onVisible).toHaveBeenCalledTimes(1)
expect(onVisible).toHaveBeenLastCalledWith(true)

await userEvent.click(checkbox)
expect(onVisible).toHaveBeenCalledTimes(2)
expect(onVisible).toHaveBeenLastCalledWith(false)
})

rerender(
<Visibility visible={true} onVisible={onVisible} animate>
Child
</Visibility>
it('should call onAnimationEnd when animation is done', async () => {
const onAnimationEnd = jest.fn()

render(
<Form.Handler
defaultData={{
toggleValue: false,
}}
>
<Field.Boolean path="/toggleValue" />
<Form.Visibility
pathTrue="/toggleValue"
onAnimationEnd={onAnimationEnd}
animate
>
content
</Form.Visibility>
</Form.Handler>
)

expect(onVisible).toHaveBeenCalledTimes(2)
expect(onVisible).toHaveBeenLastCalledWith(true)
const checkbox = document.querySelector('input[type="checkbox"]')

expect(onAnimationEnd).toHaveBeenCalledTimes(0)

await userEvent.click(checkbox)
await waitFor(() => {
expect(onAnimationEnd).toHaveBeenCalledTimes(1)
expect(onAnimationEnd).toHaveBeenLastCalledWith('opened')
})

await userEvent.click(checkbox)
await waitFor(() => {
expect(onAnimationEnd).toHaveBeenLastCalledWith('closed')
})

await userEvent.click(checkbox)
await waitFor(() => {
expect(onAnimationEnd).toHaveBeenLastCalledWith('opened')
})
})

it('should not call onAnimationEnd when "animation" is false', async () => {
const onAnimationEnd = jest.fn()

render(
<Form.Handler
defaultData={{
toggleValue: false,
}}
>
<Field.Boolean path="/toggleValue" />
<Form.Visibility
pathTrue="/toggleValue"
onAnimationEnd={onAnimationEnd}
animate
>
content
</Form.Visibility>
</Form.Handler>
)

const checkbox = document.querySelector('input[type="checkbox"]')

expect(onAnimationEnd).toHaveBeenCalledTimes(0)

await userEvent.click(checkbox)

expect(() => {
expect(onAnimationEnd).toHaveBeenCalledTimes(0)
}).toNeverResolve()
})
})

Expand Down

0 comments on commit 87728b4

Please sign in to comment.