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

feat(Forms): add onAnimationEnd property to Form.Visibility #4356

Merged
merged 6 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -3,8 +3,15 @@ showTabs: true
---

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

## Properties

<PropertiesTable props={VisibilityProperties} />

## 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. It can be `opening`, `closing` or `adjusting`.',
langz marked this conversation as resolved.
Show resolved Hide resolved
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. It can be `opened`, `closed` or `adjusted`.',
langz marked this conversation as resolved.
Show resolved Hide resolved
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 the content is visible. Only for when `animate` is true. */
onAnimationEnd?: HeightAnimationAllProps['onAnimationEnd']
tujoworker marked this conversation as resolved.
Show resolved Hide resolved
/** 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: {
langz marked this conversation as resolved.
Show resolved Hide resolved
doc: 'Callback for when the content gets visible. Returns a boolean as the first parameter.',
type: 'function',
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
Loading