From 87728b4b350cf7a03d467fe2d672d98a8ec2af1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Wed, 4 Dec 2024 14:19:13 +0100 Subject: [PATCH] feat(Forms): add `onAnimationEnd` property to Form.Visibility (#4356) 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 --- .../extensions/forms/Form/Visibility.mdx | 2 + .../forms/Form/Visibility/events.mdx | 10 ++ .../height-animation/HeightAnimationDocs.ts | 4 +- .../forms/Form/Visibility/Visibility.tsx | 35 +++- .../forms/Form/Visibility/VisibilityDocs.ts | 15 +- .../Visibility/__tests__/Visibility.test.tsx | 165 ++++++++++++++---- 6 files changed, 186 insertions(+), 45 deletions(-) create mode 100644 packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility/events.mdx diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility.mdx index 9ff6fa1d38b..de444c270d6 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility.mdx @@ -9,6 +9,8 @@ tabs: key: '/demos' - title: Properties key: '/properties' + - title: Events + key: '/events' breadcrumb: - text: Forms href: /uilib/extensions/forms/ diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility/events.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility/events.mdx new file mode 100644 index 00000000000..120df0da2f6 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Visibility/events.mdx @@ -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 + + diff --git a/packages/dnb-eufemia/src/components/height-animation/HeightAnimationDocs.ts b/packages/dnb-eufemia/src/components/height-animation/HeightAnimationDocs.ts index 078afa71f57..f2fea3049b4 100644 --- a/packages/dnb-eufemia/src/components/height-animation/HeightAnimationDocs.ts +++ b/packages/dnb-eufemia/src/components/height-animation/HeightAnimationDocs.ts @@ -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', }, diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx index d3c03cf2fe6..1032ce5f9ef 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Form/Visibility/Visibility.tsx @@ -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 = | { @@ -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 */ @@ -106,6 +109,7 @@ function Visibility({ inferData, filterData, onVisible, + onAnimationEnd, animate, keepInDOM, compensateForGap, @@ -144,6 +148,16 @@ function Visibility({ {children} ) + const mountedRef = useMounted() + + const onOpen: HeightAnimationAllProps['onOpen'] = useCallback( + (state) => { + if (mountedRef.current) { + onVisible?.(state) + } + }, + [mountedRef, onVisible] + ) if (animate) { const props = !open ? fieldPropsWhenHidden : null @@ -151,7 +165,8 @@ function Visibility({ return ( { `) }) - 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( + + + Child + + + ) + + 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( - - - Child - - + + + + content + + ) - 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( - - Child - + render( + + + + content + + ) + 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( - - Child - + it('should call onAnimationEnd when animation is done', async () => { + const onAnimationEnd = jest.fn() + + render( + + + + content + + ) - 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( + + + + content + + + ) + + const checkbox = document.querySelector('input[type="checkbox"]') + + expect(onAnimationEnd).toHaveBeenCalledTimes(0) + + await userEvent.click(checkbox) + + expect(() => { + expect(onAnimationEnd).toHaveBeenCalledTimes(0) + }).toNeverResolve() }) })