diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation.md b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation.md new file mode 100644 index 00000000000..24961facfd0 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation.md @@ -0,0 +1,12 @@ +--- +title: 'HeightAnimation' +description: 'HeightAnimation is a helper component to animate from 0 to height:auto powered by CSS.' +status: 'new' +showTabs: true +--- + +import HeightAnimationInfo from 'Docs/uilib/components/height-animation/info' +import HeightAnimationDemos from 'Docs/uilib/components/height-animation/demos' + + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/Examples.tsx new file mode 100644 index 00000000000..ed19cbe4fad --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/Examples.tsx @@ -0,0 +1,60 @@ +/** + * UI lib Component Example + * + */ + +import ComponentBox from 'dnb-design-system-portal/src/shared/tags/ComponentBox' + +export function HeightAnimationDefault() { + return ( + + { + /* jsx */ ` +const Example = () => { + const [openState, setOpenState] = React.useState(false) + + const onChangeHandler = ({ checked }) => { + setOpenState(checked) + } + + return ( + <> + + Toggle me + + + + +

+ Your content +

+
+
+ + ) +} + +const StyledSection = styled(Section)\` + .content-element { + transition: transform 400ms var(--easing-default); + transform: translateY(-2rem); + + padding: 4rem 0; + } + + .dnb-height-animation--parallax .content-element { + transform: translateY(0); + } +\` + +render() + ` + } +
+ ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/demos.md b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/demos.md new file mode 100644 index 00000000000..299f62fa82b --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/demos.md @@ -0,0 +1,13 @@ +--- +showTabs: true +--- + +import { +HeightAnimationDefault, +} from 'Docs/uilib/components/height-animation/Examples' + +## Demos + +### HeightAnimation + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/events.md b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/events.md new file mode 100644 index 00000000000..875df1bbea9 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/events.md @@ -0,0 +1,7 @@ +--- +showTabs: true +--- + +## Events + +No events are supported at the moment. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/info.md b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/info.md new file mode 100644 index 00000000000..f4393e22c11 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/info.md @@ -0,0 +1,17 @@ +--- +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. + +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: + +## 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`. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/properties.md b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/properties.md new file mode 100644 index 00000000000..e9f9c1d70b4 --- /dev/null +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/height-animation/properties.md @@ -0,0 +1,12 @@ +--- +showTabs: true +--- + +## Properties + +| Properties | Description | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `open` | _(optional)_ Set to `true` when the view should animate from 0px to auto. Defaults to `false`. | +| `animate` | _(optional)_ Set to `false` to omit the animation. Defaults to `true`. | +| `element` | _(optional)_ Custom HTML element for the component. Defaults to `div` HTML Element. | +| [Space](/uilib/components/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | diff --git a/packages/dnb-design-system-portal/src/docs/uilib/helpers/Examples.js b/packages/dnb-design-system-portal/src/docs/uilib/helpers/Examples.js index 3d5a51ee0e2..7c5fbd209ce 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/helpers/Examples.js +++ b/packages/dnb-design-system-portal/src/docs/uilib/helpers/Examples.js @@ -5,110 +5,13 @@ import React from 'react' import styled from '@emotion/styled' -import classnames from 'classnames' import ComponentBox from 'dnb-design-system-portal/src/shared/tags/ComponentBox' -import { useHeightAnimation } from '@dnb/eufemia/src/shared/useHeightAnimation' // have a limit because this page is used for screenshot tests const Wrapper = styled.div` max-width: 40rem; ` -export function HeightAnimationExample() { - return ( - - { - /* jsx */ ` -const AnimatedContent = ({ - open = false, - noAnimation = false, - ...rest -}) => { - const animationElement = React.useRef() - const { isOpen, isInDOM, isVisibleParallax } = useHeightAnimation( - animationElement, - { - open, - animate: !noAnimation, - } - ) - - // Optional: You can also entirely remove it from the DOM - // if (!isInDOM) { - // return null - // } - - return ( - - {isInDOM /* <-- Optional */ && ( -
-

Your content

-
- )} -
- ) -} - -const HeightAnimation = ({ open = false, ...rest }) => { -const [openState, setOpenState] = React.useState(open) - -const onChangeHandler = ({ checked }) => { - setOpenState(checked) -} - -return ( - <> - - Toggle me - - - - -) -} - -const AnimatedDiv = styled(Section)\` - .animation-element { - overflow: hidden; - transition: height 1s var(--easing-default); - } - - .content-element { - transition: transform 1s var(--easing-default); - transform: translateY(-2rem); - } - - &.is-in-parallax .content-element { - transform: translateY(0); - } - - .content-element { - padding: 4rem 0; - } -\` - -render() - ` - } -
- ) -} - export function CoreStyleExample() { return ( diff --git a/packages/dnb-design-system-portal/src/docs/uilib/helpers/hooks.md b/packages/dnb-design-system-portal/src/docs/uilib/helpers/hooks.md index 8e58985993d..c6e93abf69c 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/helpers/hooks.md +++ b/packages/dnb-design-system-portal/src/docs/uilib/helpers/hooks.md @@ -1,29 +1,4 @@ --- showTabs: true +draft: true --- - -import { -HeightAnimationExample, -} from 'Docs/uilib/helpers/Examples' -import SkipLinkExample from 'Docs/uilib/usage/accessibility/examples/skip-link-example.js' - -## Description - -These React Hooks are internally used in the components, and are with that a good choice when it comes to save bandwidth in the final production bundle. - -## `useHeightAnimation` - -In many places we want to animate the content in and out. The challenge is to never define a fixed height, because of an unknown content size and users' different font sizes. - -The `useHeightAnimation` hook takes an HTML Element, and animates it from 0 to the current content. When the animation is done, it sets the element's height to `auto`. - -The element animation is done with a CSS transition, e.g.: - -```css -.animation-element { - overflow: hidden; - transition: height 1s var(--easing-default); -} -``` - - diff --git a/packages/dnb-eufemia/src/components/HeightAnimation.js b/packages/dnb-eufemia/src/components/HeightAnimation.js new file mode 100644 index 00000000000..6029cd12ee7 --- /dev/null +++ b/packages/dnb-eufemia/src/components/HeightAnimation.js @@ -0,0 +1,14 @@ +/** + * ATTENTION: This file is auto generated by using "prepareTemplates". + * Do not change the content! + * + */ + +/** + * Library Index height-animation to autogenerate all the components and extensions + * Used by "prepareHeightAnimations" + */ + +import HeightAnimation from './height-animation/HeightAnimation' +export * from './height-animation/HeightAnimation' +export default HeightAnimation diff --git a/packages/dnb-eufemia/src/components/height-animation/HeightAnimation.tsx b/packages/dnb-eufemia/src/components/height-animation/HeightAnimation.tsx new file mode 100644 index 00000000000..0aafbeac37d --- /dev/null +++ b/packages/dnb-eufemia/src/components/height-animation/HeightAnimation.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import classnames from 'classnames' +import { DynamicElement } from '../../shared/interfaces' +import { useHeightAnimation } from './useHeightAnimation' +import Space from '../space/Space' + +export type HeightAnimationProps = { + open: boolean + animate?: boolean + element?: DynamicElement + className?: React.ReactNode + children?: React.ReactNode | HTMLElement +} + +export default function HeightAnimation({ + open = false, + animate = true, + element, + className, + children, + ...props +}: HeightAnimationProps) { + const innerRef = React.useRef() + const { isInDOM, isVisible, isVisibleParallax } = useHeightAnimation( + innerRef, + { + open, + animate, + } + ) + + if (!isInDOM) { + return null + } + + return ( + + {children} + + ) +} diff --git a/packages/dnb-eufemia/src/shared/__tests__/useHeightAnimation.test.tsx b/packages/dnb-eufemia/src/components/height-animation/__tests__/HeightAnimation.test.tsx similarity index 98% rename from packages/dnb-eufemia/src/shared/__tests__/useHeightAnimation.test.tsx rename to packages/dnb-eufemia/src/components/height-animation/__tests__/HeightAnimation.test.tsx index 58a97f0ab6b..948c84bc26f 100644 --- a/packages/dnb-eufemia/src/shared/__tests__/useHeightAnimation.test.tsx +++ b/packages/dnb-eufemia/src/components/height-animation/__tests__/HeightAnimation.test.tsx @@ -6,9 +6,9 @@ import React from 'react' import classnames from 'classnames' import { render, act, fireEvent } from '@testing-library/react' -import { useHeightAnimation } from '../useHeightAnimation' -import ToggleButton from '../../components/ToggleButton' +import ToggleButton from '../../ToggleButton' import { wait } from '@testing-library/user-event/dist/utils' +import { useHeightAnimation } from '../useHeightAnimation' beforeEach(() => { global.IS_TEST = false diff --git a/packages/dnb-eufemia/src/components/height-animation/index.js b/packages/dnb-eufemia/src/components/height-animation/index.js new file mode 100644 index 00000000000..964d5f4a6ff --- /dev/null +++ b/packages/dnb-eufemia/src/components/height-animation/index.js @@ -0,0 +1,8 @@ +/** + * Component Entry + * + */ + +import HeightAnimation from './HeightAnimation' +export default HeightAnimation +export * from './HeightAnimation' 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 new file mode 100644 index 00000000000..9e8bd3a334b --- /dev/null +++ b/packages/dnb-eufemia/src/components/height-animation/stories/HeightAnimation.stories.tsx @@ -0,0 +1,56 @@ +/** + * @dnb/eufemia Component Story + * + */ + +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 HeightAnimation from '../HeightAnimation' + +export default { + title: 'Eufemia/Components/HeightAnimation', +} + +export const HeightAnimationSandbox = () => { + const [openState, setOpenState] = React.useState(false) + + const onChangeHandler = ({ checked }) => { + setOpenState(checked) + } + + return ( + <> + + Toggle me + + + + +

+ Your content +

+
+
+ + ) +} + +const StyledSection = styled(Section)` + .content-element { + transition: transform 400ms var(--easing-default); + transform: translateY(-2rem); + + padding: 4rem 0; + } + + .dnb-height-animation--parallax .content-element { + transform: translateY(0); + } +` diff --git a/packages/dnb-eufemia/src/components/height-animation/style.js b/packages/dnb-eufemia/src/components/height-animation/style.js new file mode 100644 index 00000000000..0f4ea141ebe --- /dev/null +++ b/packages/dnb-eufemia/src/components/height-animation/style.js @@ -0,0 +1,6 @@ +/** + * Web Style Import + * + */ + +import './style/dnb-height-animation.scss' diff --git a/packages/dnb-eufemia/src/components/height-animation/style/_height-animation.scss b/packages/dnb-eufemia/src/components/height-animation/style/_height-animation.scss new file mode 100644 index 00000000000..8e66f433989 --- /dev/null +++ b/packages/dnb-eufemia/src/components/height-animation/style/_height-animation.scss @@ -0,0 +1,9 @@ +/* +* HeightAnimation component +* +*/ + +.dnb-height-animation { + overflow: hidden; + transition: height 400ms var(--easing-default); +} diff --git a/packages/dnb-eufemia/src/components/height-animation/style/dnb-height-animation.scss b/packages/dnb-eufemia/src/components/height-animation/style/dnb-height-animation.scss new file mode 100644 index 00000000000..ab1588417d4 --- /dev/null +++ b/packages/dnb-eufemia/src/components/height-animation/style/dnb-height-animation.scss @@ -0,0 +1,12 @@ +/* +* DNB Visually Hidden +* +*/ + +@import '../../../style/components/imports.scss'; + +.dnb-height-animation { + @include componentReset(); +} + +@import './_height-animation.scss'; diff --git a/packages/dnb-eufemia/src/components/height-animation/style/index.js b/packages/dnb-eufemia/src/components/height-animation/style/index.js new file mode 100644 index 00000000000..cbf1a4a9453 --- /dev/null +++ b/packages/dnb-eufemia/src/components/height-animation/style/index.js @@ -0,0 +1,6 @@ +/** + * Web Style Import + * + */ + +import './dnb-height-animation.scss' diff --git a/packages/dnb-eufemia/src/shared/useHeightAnimation.tsx b/packages/dnb-eufemia/src/components/height-animation/useHeightAnimation.tsx similarity index 94% rename from packages/dnb-eufemia/src/shared/useHeightAnimation.tsx rename to packages/dnb-eufemia/src/components/height-animation/useHeightAnimation.tsx index 2a4c6458d17..b6026c1f8ed 100644 --- a/packages/dnb-eufemia/src/shared/useHeightAnimation.tsx +++ b/packages/dnb-eufemia/src/components/height-animation/useHeightAnimation.tsx @@ -1,5 +1,5 @@ import React from 'react' -import AnimateHeight from './AnimateHeight' +import AnimateHeight from '../../shared/AnimateHeight' type useHeightAnimationOptions = { open?: boolean @@ -32,6 +32,7 @@ export function useHeightAnimation( setIsVisible(true) setParallax(true) break + case 'closing': setParallax(false) break @@ -43,6 +44,7 @@ export function useHeightAnimation( case 'opened': setIsOpen(true) break + case 'closed': setIsVisible(false) setIsOpen(false) @@ -52,9 +54,7 @@ export function useHeightAnimation( }) } - return () => { - animRef.current?.remove() - } + return () => animRef.current?.remove() }, [animate]) React.useLayoutEffect(() => { diff --git a/packages/dnb-eufemia/src/components/index.js b/packages/dnb-eufemia/src/components/index.js index 5298715262c..992019406e8 100644 --- a/packages/dnb-eufemia/src/components/index.js +++ b/packages/dnb-eufemia/src/components/index.js @@ -28,6 +28,7 @@ import FormStatus from './form-status/FormStatus' import GlobalError from './global-error/GlobalError' import GlobalStatus from './global-status/GlobalStatus' import Heading from './heading/Heading' +import HeightAnimation from './height-animation/HeightAnimation' import HelpButton from './help-button/HelpButton' import Icon from './icon/Icon' import IconPrimary from './icon-primary/IconPrimary' @@ -75,6 +76,7 @@ export { GlobalError, GlobalStatus, Heading, + HeightAnimation, HelpButton, Icon, IconPrimary, diff --git a/packages/dnb-eufemia/src/components/lib.js b/packages/dnb-eufemia/src/components/lib.js index e36dcd7b545..a6af619a972 100644 --- a/packages/dnb-eufemia/src/components/lib.js +++ b/packages/dnb-eufemia/src/components/lib.js @@ -30,6 +30,7 @@ import FormStatus from './form-status/FormStatus' import GlobalError from './global-error/GlobalError' import GlobalStatus from './global-status/GlobalStatus' import Heading from './heading/Heading' +import HeightAnimation from './height-animation/HeightAnimation' import HelpButton from './help-button/HelpButton' import Icon from './icon/Icon' import IconPrimary from './icon-primary/IconPrimary' @@ -77,6 +78,7 @@ export { GlobalError, GlobalStatus, Heading, + HeightAnimation, HelpButton, Icon, IconPrimary, @@ -125,6 +127,7 @@ export const getComponents = () => { GlobalError, GlobalStatus, Heading, + HeightAnimation, HelpButton, Icon, IconPrimary, diff --git a/packages/dnb-eufemia/src/components/skeleton/__tests__/__snapshots__/Skeleton.test.js.snap b/packages/dnb-eufemia/src/components/skeleton/__tests__/__snapshots__/Skeleton.test.js.snap index a67848a4ffc..365489ae2b3 100644 --- a/packages/dnb-eufemia/src/components/skeleton/__tests__/__snapshots__/Skeleton.test.js.snap +++ b/packages/dnb-eufemia/src/components/skeleton/__tests__/__snapshots__/Skeleton.test.js.snap @@ -20,6 +20,7 @@ exports[`Skeleton component have to match snapshot 1`] = ` element="div" id={null} inline={null} + innerRef={null} lang="nb-NO" left={null} no_collapse={null} @@ -29,7 +30,7 @@ exports[`Skeleton component have to match snapshot 1`] = ` stretch={null} top={null} > - - + `; diff --git a/packages/dnb-eufemia/src/components/space/Space.js b/packages/dnb-eufemia/src/components/space/Space.js index 9fd6135b51d..e66ca3dff26 100644 --- a/packages/dnb-eufemia/src/components/space/Space.js +++ b/packages/dnb-eufemia/src/components/space/Space.js @@ -33,7 +33,7 @@ export default class Space extends React.PureComponent { static propTypes = { id: PropTypes.string, - element: PropTypes.string, + element: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), inline: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), no_collapse: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), @@ -41,7 +41,7 @@ export default class Space extends React.PureComponent { stretch: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), skeleton: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - class: PropTypes.string, + innerRef: PropTypes.object, className: PropTypes.string, children: PropTypes.oneOfType([ PropTypes.string, @@ -62,6 +62,7 @@ export default class Space extends React.PureComponent { skeleton: null, class: null, + innerRef: null, className: null, children: null, } @@ -97,6 +98,7 @@ export default class Space extends React.PureComponent { space, stretch, skeleton, + innerRef, id: _id, // eslint-disable-line className, class: _className, @@ -126,14 +128,31 @@ export default class Space extends React.PureComponent { validateDOMAttributes(this.props, params) return ( - + {children} ) } } -const Element = ({ element: E, no_collapse, children, ...props }) => { +const Element = ({ + element: E, + no_collapse, + children, + innerRef, + ...props +}) => { + const component = ( + + {children} + + ) + if (isTrue(no_collapse)) { const R = E === 'span' || isInline(Element) ? 'span' : 'div' return ( @@ -143,20 +162,10 @@ const Element = ({ element: E, no_collapse, children, ...props }) => { isInline(Element) && 'dnb-space--inline' )} > - {children} + {component} ) } - return {children} -} -Element.propTypes = { - children: PropTypes.node, - element: PropTypes.string, - no_collapse: PropTypes.bool, -} -Element.defaultProps = { - children: null, - element: 'div', - no_collapse: true, + return component } diff --git a/packages/dnb-eufemia/src/components/space/__tests__/Space.test.js b/packages/dnb-eufemia/src/components/space/__tests__/Space.test.js index d26b2b1f96a..a89399758cc 100644 --- a/packages/dnb-eufemia/src/components/space/__tests__/Space.test.js +++ b/packages/dnb-eufemia/src/components/space/__tests__/Space.test.js @@ -12,9 +12,7 @@ import { } from '../../../core/jest/jestSetup' import Component from '../Space' -const snapshotProps = fakeProps(require.resolve('../Space'), { - optional: true, -}) +const snapshotProps = fakeProps(require.resolve('../Space')) snapshotProps.id = 'space' snapshotProps.element = 'div' snapshotProps.no_collapse = false diff --git a/packages/dnb-eufemia/src/components/space/__tests__/__snapshots__/Space.test.js.snap b/packages/dnb-eufemia/src/components/space/__tests__/__snapshots__/Space.test.js.snap index 3809e80ca7e..15d4b958626 100644 --- a/packages/dnb-eufemia/src/components/space/__tests__/__snapshots__/Space.test.js.snap +++ b/packages/dnb-eufemia/src/components/space/__tests__/__snapshots__/Space.test.js.snap @@ -3,30 +3,29 @@ exports[`Space component have to match snapshot 1`] = ` -
- children -
-
+ className="dnb-space" + /> +
`; diff --git a/packages/dnb-eufemia/src/index.js b/packages/dnb-eufemia/src/index.js index 8f7ba340507..a9c8a5eddcd 100644 --- a/packages/dnb-eufemia/src/index.js +++ b/packages/dnb-eufemia/src/index.js @@ -56,6 +56,7 @@ import FormStatus from './components/form-status/FormStatus' import GlobalError from './components/global-error/GlobalError' import GlobalStatus from './components/global-status/GlobalStatus' import Heading from './components/heading/Heading' +import HeightAnimation from './components/height-animation/HeightAnimation' import HelpButton from './components/help-button/HelpButton' import Icon from './components/icon/Icon' import IconPrimary from './components/icon-primary/IconPrimary' @@ -131,6 +132,7 @@ export { GlobalError, GlobalStatus, Heading, + HeightAnimation, HelpButton, Icon, IconPrimary, diff --git a/packages/dnb-eufemia/src/shared/interfaces.tsx b/packages/dnb-eufemia/src/shared/interfaces.tsx index 3a377f325bb..a23da138118 100644 --- a/packages/dnb-eufemia/src/shared/interfaces.tsx +++ b/packages/dnb-eufemia/src/shared/interfaces.tsx @@ -30,3 +30,7 @@ export type DataAttributeTypes = { */ // [property: `data-${string}`]: string } + +export type DynamicElement = + | keyof JSX.IntrinsicElements + | React.FunctionComponent diff --git a/packages/dnb-eufemia/src/style/dnb-ui-components.scss b/packages/dnb-eufemia/src/style/dnb-ui-components.scss index 94da4b75de0..6ca4f51e95d 100644 --- a/packages/dnb-eufemia/src/style/dnb-ui-components.scss +++ b/packages/dnb-eufemia/src/style/dnb-ui-components.scss @@ -24,6 +24,7 @@ @import '../components/global-error/style/_global-error.scss'; @import '../components/global-status/style/_global-status.scss'; @import '../components/heading/style/_heading.scss'; +@import '../components/height-animation/style/_height-animation.scss'; @import '../components/help-button/style/_help-button.scss'; @import '../components/icon/style/_icon.scss'; @import '../components/info-card/style/_info-card.scss';