FilterPicker: Add Animation
This commit is contained in:
parent
8b36066f0b
commit
186c803c31
|
@ -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
|
||||
*/
|
||||
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>
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue