Merge pull request woocommerce/woocommerce-admin#226 from woocommerce/add/filter-picker-animation
Components: Filter picker: Add animation
This commit is contained in:
commit
11b30baa6f
|
@ -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
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
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 { Dropdown, Button, Dashicon } from '@wordpress/components';
|
||||||
import { stringify as stringifyQueryObject } from 'qs';
|
import { stringify as stringifyQueryObject } from 'qs';
|
||||||
import { omit, find, partial } from 'lodash';
|
import { omit, find, partial } from 'lodash';
|
||||||
|
@ -14,6 +14,7 @@ import PropTypes from 'prop-types';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import DropdownButton from 'components/dropdown-button';
|
import DropdownButton from 'components/dropdown-button';
|
||||||
|
import AnimationSlider from 'components/animation-slider';
|
||||||
import Link from 'components/link';
|
import Link from 'components/link';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
|
@ -24,8 +25,8 @@ class FilterPicker extends Component {
|
||||||
const { filterPaths, getQueryParamValue } = props;
|
const { filterPaths, getQueryParamValue } = props;
|
||||||
this.state = {
|
this.state = {
|
||||||
nav: filterPaths[ getQueryParamValue() ],
|
nav: filterPaths[ getQueryParamValue() ],
|
||||||
|
animate: null,
|
||||||
};
|
};
|
||||||
this.listRef = createRef();
|
|
||||||
|
|
||||||
this.getSelectionPath = this.getSelectionPath.bind( this );
|
this.getSelectionPath = this.getSelectionPath.bind( this );
|
||||||
this.getOtherQueries = this.getOtherQueries.bind( this );
|
this.getOtherQueries = this.getOtherQueries.bind( this );
|
||||||
|
@ -66,8 +67,7 @@ class FilterPicker extends Component {
|
||||||
selectSubFilters( value ) {
|
selectSubFilters( value ) {
|
||||||
const nav = [ ...this.state.nav ];
|
const nav = [ ...this.state.nav ];
|
||||||
nav.push( value );
|
nav.push( value );
|
||||||
this.setState( { nav } );
|
this.setState( { nav, animate: 'left' } );
|
||||||
this.focusFirstFilter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getVisibleFilters( filters, nav ) {
|
getVisibleFilters( filters, nav ) {
|
||||||
|
@ -82,17 +82,7 @@ class FilterPicker extends Component {
|
||||||
goBack() {
|
goBack() {
|
||||||
const nav = [ ...this.state.nav ];
|
const nav = [ ...this.state.nav ];
|
||||||
nav.pop();
|
nav.pop();
|
||||||
this.setState( { nav } );
|
this.setState( { nav, animate: 'right' } );
|
||||||
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 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderButton( filter, onClose ) {
|
renderButton( filter, onClose ) {
|
||||||
|
@ -129,7 +119,7 @@ class FilterPicker extends Component {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="woocommerce-filter-picker__content-list-item-btn components-button"
|
className="woocommerce-filter-picker__content-list-item-btn components-button"
|
||||||
to={ this.getSelectionPath( filter ) }
|
href={ this.getSelectionPath( filter ) }
|
||||||
onClick={ onClose }
|
onClick={ onClose }
|
||||||
>
|
>
|
||||||
{ filter.label }
|
{ filter.label }
|
||||||
|
@ -139,7 +129,8 @@ class FilterPicker extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { filters } = this.props;
|
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();
|
const selectedFilter = this.getSelectedFilter();
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-filter-picker">
|
<div className="woocommerce-filter-picker">
|
||||||
|
@ -157,18 +148,22 @@ class FilterPicker extends Component {
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
renderContent={ ( { onClose } ) => (
|
renderContent={ ( { onClose } ) => (
|
||||||
<ul className="woocommerce-filter-picker__content-list" ref={ this.listRef }>
|
<AnimationSlider animationKey={ nav } animate={ animate } focusOnChange>
|
||||||
{ visibleFilters.map( ( filter, i ) => (
|
{ () => (
|
||||||
<li
|
<ul className="woocommerce-filter-picker__content-list">
|
||||||
key={ i }
|
{ visibleFilters.map( filter => (
|
||||||
className={ classnames( 'woocommerce-filter-picker__content-list-item', {
|
<li
|
||||||
'is-selected': selectedFilter.value === filter.value,
|
key={ filter.value }
|
||||||
} ) }
|
className={ classnames( 'woocommerce-filter-picker__content-list-item', {
|
||||||
>
|
'is-selected': selectedFilter.value === filter.value,
|
||||||
{ this.renderButton( filter, onClose ) }
|
} ) }
|
||||||
</li>
|
>
|
||||||
) ) }
|
{ this.renderButton( filter, onClose ) }
|
||||||
</ul>
|
</li>
|
||||||
|
) ) }
|
||||||
|
</ul>
|
||||||
|
) }
|
||||||
|
</AnimationSlider>
|
||||||
) }
|
) }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,33 +42,35 @@
|
||||||
|
|
||||||
.woocommerce-filter-picker__content-list {
|
.woocommerce-filter-picker__content-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.woocommerce-filter-picker__content-list-item {
|
.woocommerce-filter-picker__content-list-item {
|
||||||
border-bottom: 1px solid $core-grey-light-700;
|
border-bottom: 1px solid $core-grey-light-700;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-selected {
|
&.is-selected {
|
||||||
.woocommerce-filter-picker__content-list-item-btn {
|
.woocommerce-filter-picker__content-list-item-btn {
|
||||||
|
background-color: $white;
|
||||||
|
|
||||||
|
&.components-button:not(:disabled):not([aria-disabled='true']):focus {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
&.components-button:not(:disabled):not([aria-disabled='true']):focus {
|
&::before {
|
||||||
background-color: $white;
|
content: '';
|
||||||
}
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
&::before {
|
background-color: $woocommerce;
|
||||||
content: '';
|
position: absolute;
|
||||||
width: 8px;
|
top: 50%;
|
||||||
height: 8px;
|
left: 1em;
|
||||||
background-color: $woocommerce;
|
transform: translate(50%, -50%);
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 1em;
|
|
||||||
transform: translate(50%, -50%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4772,6 +4772,11 @@
|
||||||
"esutils": "^2.0.2"
|
"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": {
|
"dom-react": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-react/-/dom-react-2.2.1.tgz",
|
"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==",
|
"integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==",
|
||||||
"dev": true
|
"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": {
|
"react-moment-proptypes": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.6.0.tgz",
|
"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-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": {
|
"react-with-direction": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.0.tgz",
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"prop-types": "^15.6.1",
|
"prop-types": "^15.6.1",
|
||||||
"react-dates": "^16.7.0",
|
"react-dates": "^16.7.0",
|
||||||
"react-slot-fill": "^2.0.1"
|
"react-slot-fill": "^2.0.1",
|
||||||
|
"react-transition-group": "^2.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue