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

Try menu appear animation & start work on handbook #13617

Merged
merged 26 commits into from
Feb 7, 2019
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dacfd32
Try menu appear animation & start work on handbook
Jan 21, 2019
1d1f164
Faster animation & commit docs.
Feb 1, 2019
5f17198
Try an animate component
youknowriad Feb 1, 2019
b028d51
Fix popover initial position
youknowriad Feb 1, 2019
bf10dff
Minor documentation spelling + grammar fixes.
kjellr Feb 1, 2019
ce7d7f1
Fix typo and clarify doc.
jasmussen Feb 4, 2019
a883d30
Fix failing unit tests by updating snapshots
gziolo Feb 4, 2019
df62716
Add template for the component README
gziolo Feb 4, 2019
6842659
Add template for the component README
gziolo Feb 4, 2019
e26b700
Add template for the component README
gziolo Feb 4, 2019
e62c739
update docs manifest
Feb 4, 2019
5415930
Fix block inserter and transformation e2e tests
youknowriad Feb 7, 2019
4ad8653
Fix tests relying on the more menu block
youknowriad Feb 7, 2019
491aab5
Fix block settings menu test and nux
youknowriad Feb 7, 2019
0e5038e
Fix editor modes and invalid block tests
youknowriad Feb 7, 2019
1e52573
Enhance the README of the animate component
youknowriad Feb 7, 2019
520503b
Fix opening the block settings menu in e2e tests
youknowriad Feb 7, 2019
88a57f9
Remove the animation doc
youknowriad Feb 7, 2019
a2de8fc
Bring back the principles section of the animation doc
youknowriad Feb 7, 2019
d9e3601
Fixed typo in link
gziolo Feb 7, 2019
1e9976a
Fixed origin option description
gziolo Feb 7, 2019
58470a3
Fix description of options once again
gziolo Feb 7, 2019
432c0e1
More test stabilization
youknowriad Feb 7, 2019
91b7144
Add the animation doc to the designer handbook
youknowriad Feb 7, 2019
8f9ef37
Update the components package changelog
youknowriad Feb 7, 2019
328b652
More e2e tests stability
youknowriad Feb 7, 2019
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
33 changes: 33 additions & 0 deletions docs/designers-developers/designers/animation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Animation
mkaz marked this conversation as resolved.
Show resolved Hide resolved

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.
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,12 @@
"markdown_source": "https://github.com/raw/WordPress/gutenberg/master/packages/wordcount/README.md",
"parent": "packages"
},
{
"title": "Animate",
"slug": "animate",
"markdown_source": "https://github.com/raw/WordPress/gutenberg/master/packages/components/src/animate/README.md",
"parent": "components"
},
{
"title": "Autocomplete",
"slug": "autocomplete",
Expand Down
39 changes: 39 additions & 0 deletions packages/components/src/animate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Animate

Simple interface to introduce animations to components.

## Usage

```jsx
import { Animate } from '@wordpress/components';

const MyAnimatedNotice = () => (
<Animate todo="Add missing props">
{ ( { className } ) => (
<Notice className={ className } status="success">
<p>Animation finished.</p>
</Notice>
) }
</Animate>
);
```

## 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When children isn't a function it will explode at the moment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine, it's a requirement for the component. it's like saying the options is an object and if you pass a number it will explode.

I do see the specificity in JSX about the children prop but ultimately I think we can just consider it a requirement to be a function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a conceptual issue with render props :)


## 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
Copy link
Member

@aduth aduth Jan 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason that options ought to be a separate object, vs. just having its own properties be props of the component itself?

In other words, is...

<Animate type="appear" options={ { origin: 'top' } } />

...necessarily any better than:

<Animate type="appear" origin="top" />

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking at the time is that "options" are type-specific, their format change for each animation type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking at the time is that "options" are type-specific, their format change for each animation type.

Okay, thanks for clarifying. I'm not sure it would necessarily be wrong to still flatten these as top-level props, even if each type had their own handling and own set of options, but the rationale you give is sensible.

It may have been misleading to me because in the current implementation, the only option is origin, and it is consistent for both the appear and slide-in types, though I suppose that's more a reflection of its current state, and these may be expanded at some point in the future.


Name | Type | Default | Description
--- | --- | --- | ---
`origin` | `string` | `top center` | Point of origin (`top`, `bottom`,` middle right`, `left`, `center`).
25 changes: 25 additions & 0 deletions packages/components/src/animate/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* External dependencies
*/
import classnames from 'classnames';

function Animate( { type, options = {}, children } ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it exit early when children isn't a function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as I was saying above, we don't guard about the other props. I'm hesitant to add a special case for 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;
28 changes: 28 additions & 0 deletions packages/components/src/animate/style.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
85 changes: 56 additions & 29 deletions packages/components/src/popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) );

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -258,6 +264,7 @@ class Popover extends Component {
focusOnMount,
getAnchorRect,
expandOnMobile,
animate = true,
/* eslint-enable no-unused-vars */
...contentProps
} = this.props;
Expand All @@ -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,
Expand All @@ -289,36 +309,43 @@ class Popover extends Component {
/* eslint-disable jsx-a11y/no-static-element-interactions */
let content = (
<PopoverDetectOutside onClickOutside={ onClickOutside }>
<IsolatedEventContainer
className={ classes }
style={ {
top: ! isMobile && popoverTop ? popoverTop + 'px' : undefined,
left: ! isMobile && popoverLeft ? popoverLeft + 'px' : undefined,
visibility: popoverSize ? undefined : 'hidden',
} }
{ ...contentProps }
onKeyDown={ this.maybeClose }
<Animate
type={ animate && isReadyToAnimate ? 'appear' : null }
options={ { origin: animateYAxis + ' ' + animateXAxis } }
>
{ isMobile && (
<div className="components-popover__header">
<span className="components-popover__header-title">
{ headerTitle }
</span>
<IconButton className="components-popover__close" icon="no-alt" onClick={ onClose } />
</div>
{ ( { className: animateClassName } ) => (
<IsolatedEventContainer
className={ classnames( classes, animateClassName ) }
style={ {
top: ! isMobile && popoverTop ? popoverTop + 'px' : undefined,
left: ! isMobile && popoverLeft ? popoverLeft + 'px' : undefined,
visibility: popoverSize ? undefined : 'hidden',
} }
{ ...contentProps }
onKeyDown={ this.maybeClose }
>
{ isMobile && (
<div className="components-popover__header">
<span className="components-popover__header-title">
{ headerTitle }
</span>
<IconButton className="components-popover__close" icon="no-alt" onClick={ onClose } />
</div>
) }
<div
ref={ this.contentNode }
className="components-popover__content"
style={ {
maxHeight: ! isMobile && contentHeight ? contentHeight + 'px' : undefined,
maxWidth: ! isMobile && contentWidth ? contentWidth + 'px' : undefined,
} }
tabIndex="-1"
>
{ children }
</div>
</IsolatedEventContainer>
) }
<div
ref={ this.contentNode }
className="components-popover__content"
style={ {
maxHeight: ! isMobile && contentHeight ? contentHeight + 'px' : undefined,
maxWidth: ! isMobile && contentWidth ? contentWidth + 'px' : undefined,
} }
tabIndex="-1"
>
{ children }
</div>
</IsolatedEventContainer>
</Animate>
</PopoverDetectOutside>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ exports[`Popover #render() should pass additional props to portaled element 1`]
>
<div>
<div
class="components-popover is-bottom is-center"
class="components-popover is-bottom is-center components-animate__appear is-from-top"
role="tooltip"
style=""
>
Expand All @@ -30,7 +30,7 @@ exports[`Popover #render() should render content 1`] = `
>
<div>
<div
class="components-popover is-bottom is-center"
class="components-popover is-bottom is-center components-animate__appear is-from-top"
style=""
>
<div
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/style.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import "./animate/style.scss";
@import "./autocomplete/style.scss";
@import "./base-control/style.scss";
@import "./button-group/style.scss";
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/tooltip/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class Tooltip extends Component {
position={ position }
className="components-tooltip"
aria-hidden="true"
animate={ false }
>
{ text }
<Shortcut className="components-tooltip__shortcut" shortcut={ shortcut } />
Expand Down
6 changes: 6 additions & 0 deletions packages/e2e-test-utils/src/click-on-more-menu-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
*/
import { first } from 'lodash';

/**
* Internal dependencies
*/
import { waitForAnimation } from './wait-for-animation';

/**
* Clicks on More Menu item, searches for the button with the text provided and clicks it.
*
Expand All @@ -12,6 +17,7 @@ export async function clickOnMoreMenuItem( buttonLabel ) {
await expect( page ).toClick(
'.edit-post-more-menu [aria-label="Show more tools & options"]'
);
await waitForAnimation();
const moreMenuContainerSelector =
'//*[contains(concat(" ", @class, " "), " edit-post-more-menu__content ")]';
let elementToClick = first( await page.$x(
Expand Down
1 change: 1 addition & 0 deletions packages/e2e-test-utils/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export { toggleScreenOption } from './toggle-screen-option';
export { transformBlockTo } from './transform-block-to';
export { uninstallPlugin } from './uninstall-plugin';
export { visitAdminPage } from './visit-admin-page';
export { waitForAnimation } from './wait-for-animation';
export { waitForWindowDimensions } from './wait-for-window-dimensions';

export * from './mocks';
6 changes: 6 additions & 0 deletions packages/e2e-test-utils/src/search-for-block.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
/**
* Internal dependencies
*/
import { waitForAnimation } from './wait-for-animation';

/**
* Search for block in the global inserter
*
* @param {string} searchTerm The text to search the inserter for.
*/
export async function searchForBlock( searchTerm ) {
await page.click( '.edit-post-header [aria-label="Add block"]' );
await waitForAnimation();
// Waiting here is necessary because sometimes the inserter takes more time to
// render than Puppeteer takes to complete the 'click' action
await page.waitForSelector( '.editor-inserter__menu' );
Expand Down
6 changes: 6 additions & 0 deletions packages/e2e-test-utils/src/switch-editor-mode-to.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Internal dependencies
*/
import { waitForAnimation } from './wait-for-animation';

/**
* Switches editor mode.
*
Expand All @@ -7,6 +12,7 @@ export async function switchEditorModeTo( mode ) {
await page.click(
'.edit-post-more-menu [aria-label="Show more tools & options"]'
);
await waitForAnimation();
const [ button ] = await page.$x(
`//button[contains(text(), '${ mode } Editor')]`
);
Expand Down
7 changes: 7 additions & 0 deletions packages/e2e-test-utils/src/transform-block-to.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Internal dependencies
*/
import { waitForAnimation } from './wait-for-animation';

/**
* Converts editor's block type.
*
Expand All @@ -7,5 +12,7 @@ export async function transformBlockTo( name ) {
await page.mouse.move( 200, 300, { steps: 10 } );
await page.mouse.move( 250, 350, { steps: 10 } );
await page.click( '.editor-block-switcher__toggle' );
await waitForAnimation();
await page.waitForSelector( `.editor-block-types-list__item[aria-label="${ name }"]` );
await page.click( `.editor-block-types-list__item[aria-label="${ name }"]` );
}
Loading