Merge pull request woocommerce/woocommerce-admin#226 from woocommerce/add/filter-picker-animation

Components: Filter picker: Add animation
This commit is contained in:
Paul Sealock 2018-07-23 10:03:35 +12:00 committed by GitHub
commit 11b30baa6f
7 changed files with 293 additions and 52 deletions

View File

@ -0,0 +1,69 @@
AnimationSlider
============
This component creates slideable content controlled by an animate prop to direct the contents to slide left or right
## How to use:
```jsx
import AnimationSlider from 'components/animation-slider';
class MySlider extends Component {
constructor() {
super();
this.state = {
pages: [ 44, 55, 66, 77, 88 ],
page: 0,
animate: null,
};
this.forward = this.forward.bind( this );
this.back = this.forward.back( this );
}
forward() {
this.setState( state => ( {
page: state.page + 1,
animate: 'left',
} ) );
}
back() {
this.setState( state => ( {
page: state.page - 1,
animate: 'right',
} ) );
}
render() {
const { page, pages, animate } = this.state;
return (
<div>
<button onClick={ this.back } disabled={ page === 0 }>
Back
</button>
<button onClick={ this.forward } disabled={ page === pages.length + 1 }>
Forward
</button>
<AnimationSlider animationKey={ page } animate={ animate }>
{ status => (
<img
className={ `my-slider my-slider-${ status }` }
src={ `/pages/${ pages[ page ] }` }
alt={ pages[ page ] }
/>
) }
</AnimationSlider>
</div>
);
}
}
```
## `AnimationSlider` Props
* `children` (required): A function returning rendered content with argument status, reflecting `CSSTransition` status
* `animationKey` (required): A unique identifier for each slideable page
* `animate`: null, 'left', 'right', to designate which direction to slide on a change
* `focusOnChange`: When set to true, the first focusable element will be focused after an animation has finished
All other props are passed to `CSSTransition`. More info at http://reactcommunity.org/react-transition-group/css-transition

View File

@ -0,0 +1,71 @@
/** @format */
/**
* External dependencies
*/
import { Component, createRef } from '@wordpress/element';
import classnames from 'classnames';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import './style.scss';
class AnimationSlider extends Component {
constructor() {
super();
this.state = {
animate: null,
};
this.container = createRef();
this.onExited = this.onExited.bind( this );
}
onExited() {
const { onExited, focusOnChange } = this.props;
if ( onExited ) {
onExited();
}
if ( focusOnChange ) {
const focusable = this.container.current.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if ( focusable ) {
focusable.focus();
}
}
}
render() {
const { children, animationKey, animate } = this.props;
const containerClasses = classnames(
'woocommerce-slide-animation',
animate && `animate-${ animate }`
);
return (
<div className={ containerClasses } ref={ this.container }>
<TransitionGroup>
<CSSTransition
timeout={ 200 }
classNames="slide"
key={ animationKey }
{ ...this.props }
onExited={ this.onExited }
>
{ status => children( { status } ) }
</CSSTransition>
</TransitionGroup>
</div>
);
}
}
AnimationSlider.propTypes = {
children: PropTypes.func.isRequired,
animationKey: PropTypes.any.isRequired,
animate: PropTypes.oneOf( [ null, 'left', 'right' ] ),
focusOnChange: PropTypes.bool,
};
export default AnimationSlider;

View File

@ -0,0 +1,71 @@
/** @format */
@keyframes slide-in-left {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0);
}
}
@keyframes slide-out-left {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(-200%);
}
}
@keyframes slide-in-right {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0);
}
}
@keyframes slide-out-right {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0%);
}
}
$duration: 200ms;
/**
The CSSTransition element creates a containing div without a class
*/
.woocommerce-slide-animation {
& > div {
width: 100%;
white-space: nowrap;
overflow: hidden;
display: flex;
}
&.animate-left .slide-enter-active {
animation: slide-in-left;
animation-duration: $duration;
}
&.animate-left .slide-exit-active {
animation: slide-out-left;
animation-duration: $duration;
}
&.animate-right .slide-enter-active {
animation: slide-in-right;
animation-duration: $duration;
}
&.animate-right .slide-exit-active {
animation: slide-out-right;
animation-duration: $duration;
}
}

View File

@ -3,7 +3,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment, createRef } from '@wordpress/element';
import { Component, Fragment } from '@wordpress/element';
import { Dropdown, Button, Dashicon } from '@wordpress/components';
import { stringify as stringifyQueryObject } from 'qs';
import { omit, find, partial } from 'lodash';
@ -14,6 +14,7 @@ import PropTypes from 'prop-types';
* Internal dependencies
*/
import DropdownButton from 'components/dropdown-button';
import AnimationSlider from 'components/animation-slider';
import Link from 'components/link';
import './style.scss';
@ -24,8 +25,8 @@ class FilterPicker extends Component {
const { filterPaths, getQueryParamValue } = props;
this.state = {
nav: filterPaths[ getQueryParamValue() ],
animate: null,
};
this.listRef = createRef();
this.getSelectionPath = this.getSelectionPath.bind( this );
this.getOtherQueries = this.getOtherQueries.bind( this );
@ -66,8 +67,7 @@ class FilterPicker extends Component {
selectSubFilters( value ) {
const nav = [ ...this.state.nav ];
nav.push( value );
this.setState( { nav } );
this.focusFirstFilter();
this.setState( { nav, animate: 'left' } );
}
getVisibleFilters( filters, nav ) {
@ -82,17 +82,7 @@ class FilterPicker extends Component {
goBack() {
const nav = [ ...this.state.nav ];
nav.pop();
this.setState( { nav } );
this.focusFirstFilter();
}
focusFirstFilter() {
setTimeout( () => {
const list = this.listRef.current;
if ( list.children.length && list.children[ 0 ].children.length ) {
list.children[ 0 ].children[ 0 ].focus();
}
}, 0 );
this.setState( { nav, animate: 'right' } );
}
renderButton( filter, onClose ) {
@ -129,7 +119,7 @@ class FilterPicker extends Component {
return (
<Link
className="woocommerce-filter-picker__content-list-item-btn components-button"
to={ this.getSelectionPath( filter ) }
href={ this.getSelectionPath( filter ) }
onClick={ onClose }
>
{ filter.label }
@ -139,7 +129,8 @@ class FilterPicker extends Component {
render() {
const { filters } = this.props;
const visibleFilters = this.getVisibleFilters( filters, [ ...this.state.nav ] );
const { nav, animate } = this.state;
const visibleFilters = this.getVisibleFilters( filters, [ ...nav ] );
const selectedFilter = this.getSelectedFilter();
return (
<div className="woocommerce-filter-picker">
@ -157,18 +148,22 @@ class FilterPicker extends Component {
/>
) }
renderContent={ ( { onClose } ) => (
<ul className="woocommerce-filter-picker__content-list" ref={ this.listRef }>
{ visibleFilters.map( ( filter, i ) => (
<li
key={ i }
className={ classnames( 'woocommerce-filter-picker__content-list-item', {
'is-selected': selectedFilter.value === filter.value,
} ) }
>
{ this.renderButton( filter, onClose ) }
</li>
) ) }
</ul>
<AnimationSlider animationKey={ nav } animate={ animate } focusOnChange>
{ () => (
<ul className="woocommerce-filter-picker__content-list">
{ visibleFilters.map( filter => (
<li
key={ filter.value }
className={ classnames( 'woocommerce-filter-picker__content-list-item', {
'is-selected': selectedFilter.value === filter.value,
} ) }
>
{ this.renderButton( filter, onClose ) }
</li>
) ) }
</ul>
) }
</AnimationSlider>
) }
/>
</div>

View File

@ -42,33 +42,35 @@
.woocommerce-filter-picker__content-list {
margin: 0;
width: 100%;
min-width: 100%;
}
.woocommerce-filter-picker__content-list-item {
border-bottom: 1px solid $core-grey-light-700;
margin: 0;
.woocommerce-filter-picker__content-list-item {
border-bottom: 1px solid $core-grey-light-700;
margin: 0;
&:last-child {
border-bottom: none;
}
&:last-child {
border-bottom: none;
}
&.is-selected {
.woocommerce-filter-picker__content-list-item-btn {
&.is-selected {
.woocommerce-filter-picker__content-list-item-btn {
background-color: $white;
&.components-button:not(:disabled):not([aria-disabled='true']):focus {
background-color: $white;
}
&.components-button:not(:disabled):not([aria-disabled='true']):focus {
background-color: $white;
}
&::before {
content: '';
width: 8px;
height: 8px;
background-color: $woocommerce;
position: absolute;
top: 50%;
left: 1em;
transform: translate(50%, -50%);
}
&::before {
content: '';
width: 8px;
height: 8px;
background-color: $woocommerce;
position: absolute;
top: 50%;
left: 1em;
transform: translate(50%, -50%);
}
}
}

View File

@ -4772,6 +4772,11 @@
"esutils": "^2.0.2"
}
},
"dom-helpers": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.3.1.tgz",
"integrity": "sha512-2Sm+JaYn74OiTM2wHvxJOo3roiq/h25Yi69Fqk269cNUwIXsCvATB6CRSFC9Am/20G2b28hGv/+7NiWydIrPvg=="
},
"dom-react": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/dom-react/-/dom-react-2.2.1.tgz",
@ -13817,6 +13822,11 @@
"integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==",
"dev": true
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-moment-proptypes": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.6.0.tgz",
@ -13915,6 +13925,28 @@
"react-is": "^16.4.1"
}
},
"react-transition-group": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.4.0.tgz",
"integrity": "sha512-Xv5d55NkJUxUzLCImGSanK8Cl/30sgpOEMGc5m86t8+kZwrPxPCPcFqyx83kkr+5Lz5gs6djuvE5By+gce+VjA==",
"requires": {
"dom-helpers": "^3.3.1",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
},
"dependencies": {
"prop-types": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
"requires": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
}
}
}
},
"react-with-direction": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.0.tgz",

View File

@ -78,6 +78,7 @@
"lodash": "^4.17.10",
"prop-types": "^15.6.1",
"react-dates": "^16.7.0",
"react-slot-fill": "^2.0.1"
"react-slot-fill": "^2.0.1",
"react-transition-group": "^2.4.0"
}
}