195 | );
196 | }
197 | }
198 |
199 | /* eslint-disable react/forbid-prop-types */
200 | FlipMoveWrapper.propTypes = {
201 | items: PropTypes.arrayOf(
202 | PropTypes.shape({
203 | id: PropTypes.string.isRequired,
204 | text: PropTypes.string,
205 | }),
206 | ),
207 | flipMoveProps: PropTypes.object,
208 | itemType: PropTypes.oneOfType([
209 | PropTypes.string, // for DOM types like 'div'
210 | PropTypes.func, // for composite components
211 | ]),
212 | bodyContainerStyles: PropTypes.object,
213 | flipMoveContainerStyles: PropTypes.object,
214 | listItemStyles: PropTypes.object,
215 | applyContinuousItemUpdates: PropTypes.bool,
216 | sequence: PropTypes.arrayOf(
217 | PropTypes.shape({
218 | eventName: PropTypes.string,
219 | delay: PropTypes.number,
220 | args: PropTypes.array,
221 | }),
222 | ),
223 | };
224 |
225 | FlipMoveWrapper.defaultProps = {
226 | items: [
227 | {
228 | id: 'a',
229 | text: "7 Insecticides You Don't Know You're Consuming",
230 | count: 0,
231 | },
232 | { id: 'b', text: '11 Ways To Style Your Hair', count: 0 },
233 | {
234 | id: 'c',
235 | text: 'The 200 Countries You Have To Visit Before The Apocalypse',
236 | count: 0,
237 | },
238 | {
239 | id: 'd',
240 | text: 'Turtles: The Unexpected Miracle Anti-Aging Product',
241 | count: 0,
242 | },
243 | {
244 | id: 'e',
245 | text: 'Divine Intervention: Fashion Tips For The Vatican',
246 | count: 0,
247 | },
248 | ],
249 | itemType: 'div',
250 | };
251 |
252 | export default FlipMoveWrapper;
253 |
--------------------------------------------------------------------------------
/src/prop-converter.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /**
3 | * React Flip Move | propConverter
4 | * (c) 2016-present Joshua Comeau
5 | *
6 | * Abstracted away a bunch of the messy business with props.
7 | * - props flow types and defaultProps
8 | * - Type conversion (We accept 'string' and 'number' values for duration,
9 | * delay, and other fields, but we actually need them to be ints.)
10 | * - Children conversion (we need the children to be an array. May not always
11 | * be, if a single child is passed in.)
12 | * - Resolving animation presets into their base CSS styles
13 | */
14 | /* eslint-disable block-scoped-var */
15 |
16 | import React, { Component, Children } from 'react';
17 | // eslint-disable-next-line no-duplicate-imports
18 | import type { ComponentType, Element } from 'react';
19 |
20 | import {
21 | statelessFunctionalComponentSupplied,
22 | primitiveNodeSupplied,
23 | invalidTypeForTimingProp,
24 | invalidEnterLeavePreset,
25 | } from './error-messages';
26 | import {
27 | appearPresets,
28 | enterPresets,
29 | leavePresets,
30 | defaultPreset,
31 | disablePreset,
32 | } from './enter-leave-presets';
33 | import { isElementAnSFC, omit } from './helpers';
34 | import type {
35 | Animation,
36 | AnimationProp,
37 | Presets,
38 | FlipMoveProps,
39 | ConvertedProps,
40 | DelegatedProps,
41 | } from './typings';
42 |
43 | declare var process: {
44 | env: {
45 | NODE_ENV: 'production' | 'development',
46 | },
47 | };
48 |
49 | function propConverter(
50 | ComposedComponent: ComponentType,
51 | ): ComponentType {
52 | return class FlipMovePropConverter extends Component {
53 | static defaultProps = {
54 | easing: 'ease-in-out',
55 | duration: 350,
56 | delay: 0,
57 | staggerDurationBy: 0,
58 | staggerDelayBy: 0,
59 | typeName: 'div',
60 | enterAnimation: defaultPreset,
61 | leaveAnimation: defaultPreset,
62 | disableAllAnimations: false,
63 | getPosition: (node: HTMLElement) => node.getBoundingClientRect(),
64 | maintainContainerHeight: false,
65 | verticalAlignment: 'top',
66 | };
67 |
68 | // eslint-disable-next-line class-methods-use-this
69 | checkChildren(children) {
70 | // Skip all console warnings in production.
71 | // Bail early, to avoid unnecessary work.
72 | if (process.env.NODE_ENV === 'production') {
73 | return;
74 | }
75 |
76 | // same as React.Node, but without fragments, see https://github.com/facebook/flow/issues/4781
77 | type Child = void | null | boolean | number | string | Element<*>;
78 |
79 | // FlipMove does not support stateless functional components.
80 | // Check to see if any supplied components won't work.
81 | // If the child doesn't have a key, it means we aren't animating it.
82 | // It's allowed to be an SFC, since we ignore it.
83 | Children.forEach(children, (child: Child) => {
84 | // null, undefined, and booleans will be filtered out by Children.toArray
85 | if (child == null || typeof child === 'boolean') {
86 | return;
87 | }
88 |
89 | if (typeof child !== 'object') {
90 | primitiveNodeSupplied();
91 | return;
92 | }
93 |
94 | if (isElementAnSFC(child) && child.key != null) {
95 | statelessFunctionalComponentSupplied();
96 | }
97 | });
98 | }
99 |
100 | convertProps(props: FlipMoveProps): ConvertedProps {
101 | const workingProps: ConvertedProps = {
102 | // explicitly bypass the props that don't need conversion
103 | children: props.children,
104 | easing: props.easing,
105 | onStart: props.onStart,
106 | onFinish: props.onFinish,
107 | onStartAll: props.onStartAll,
108 | onFinishAll: props.onFinishAll,
109 | typeName: props.typeName,
110 | disableAllAnimations: props.disableAllAnimations,
111 | getPosition: props.getPosition,
112 | maintainContainerHeight: props.maintainContainerHeight,
113 | verticalAlignment: props.verticalAlignment,
114 |
115 | // Do string-to-int conversion for all timing-related props
116 | duration: this.convertTimingProp('duration'),
117 | delay: this.convertTimingProp('delay'),
118 | staggerDurationBy: this.convertTimingProp('staggerDurationBy'),
119 | staggerDelayBy: this.convertTimingProp('staggerDelayBy'),
120 |
121 | // Our enter/leave animations can be specified as boolean (default or
122 | // disabled), string (preset name), or object (actual animation values).
123 | // Let's standardize this so that they're always objects
124 | appearAnimation: this.convertAnimationProp(
125 | props.appearAnimation,
126 | appearPresets,
127 | ),
128 | enterAnimation: this.convertAnimationProp(
129 | props.enterAnimation,
130 | enterPresets,
131 | ),
132 | leaveAnimation: this.convertAnimationProp(
133 | props.leaveAnimation,
134 | leavePresets,
135 | ),
136 |
137 | delegated: {},
138 | };
139 |
140 | this.checkChildren(workingProps.children);
141 |
142 | // Gather any additional props;
143 | // they will be delegated to the ReactElement created.
144 | const primaryPropKeys = Object.keys(workingProps);
145 | const delegatedProps: DelegatedProps = omit(this.props, primaryPropKeys);
146 |
147 | // The FlipMove container element needs to have a non-static position.
148 | // We use `relative` by default, but it can be overridden by the user.
149 | // Now that we're delegating props, we need to merge this in.
150 | delegatedProps.style = {
151 | position: 'relative',
152 | ...delegatedProps.style,
153 | };
154 |
155 | workingProps.delegated = delegatedProps;
156 |
157 | return workingProps;
158 | }
159 |
160 | convertTimingProp(prop: string): number {
161 | const rawValue: string | number = this.props[prop];
162 |
163 | const value =
164 | typeof rawValue === 'number' ? rawValue : parseInt(rawValue, 10);
165 |
166 | if (isNaN(value)) {
167 | const defaultValue: number = FlipMovePropConverter.defaultProps[prop];
168 |
169 | if (process.env.NODE_ENV !== 'production') {
170 | invalidTypeForTimingProp({
171 | prop,
172 | value: rawValue,
173 | defaultValue,
174 | });
175 | }
176 |
177 | return defaultValue;
178 | }
179 |
180 | return value;
181 | }
182 |
183 | // eslint-disable-next-line class-methods-use-this
184 | convertAnimationProp(
185 | animation: ?AnimationProp,
186 | presets: Presets,
187 | ): ?Animation {
188 | switch (typeof animation) {
189 | case 'boolean': {
190 | // If it's true, we want to use the default preset.
191 | // If it's false, we want to use the 'none' preset.
192 | return presets[animation ? defaultPreset : disablePreset];
193 | }
194 |
195 | case 'string': {
196 | const presetKeys = Object.keys(presets);
197 |
198 | if (presetKeys.indexOf(animation) === -1) {
199 | if (process.env.NODE_ENV !== 'production') {
200 | invalidEnterLeavePreset({
201 | value: animation,
202 | acceptableValues: presetKeys.join(', '),
203 | defaultValue: defaultPreset,
204 | });
205 | }
206 |
207 | return presets[defaultPreset];
208 | }
209 |
210 | return presets[animation];
211 | }
212 |
213 | default: {
214 | return animation;
215 | }
216 | }
217 | }
218 |
219 | render() {
220 | return ;
221 | }
222 | };
223 | }
224 |
225 | export default propConverter;
226 |
--------------------------------------------------------------------------------
/src/CODE_TOUR.md:
--------------------------------------------------------------------------------
1 | # Code Tour
2 |
3 | This project started off pretty simply: Use the FLIP technique to handle list re-orderings.
4 |
5 | Over the past year, though, the code has become increasingly complex as new features were added, and edge-cases corrected.
6 |
7 | This guide serves as a high-level overview of how the code works, to make it easier for others (and future me) to continue developing this project.
8 |
9 |
10 | ## Prop Conversion via HOC
11 |
12 | FlipMove takes a lot of props, and these props need a fair bit of validation/cleanup.
13 |
14 | Rather than litter the main component with a bunch of guards and boilerplate, I created a higher-order component which handles all of that business.
15 |
16 | the file `prop-converter.js` holds all prop-validation logic. It should mostly be pretty self-explanatory: It takes in props, validates them, makes a few tweaks, and passes them down to FlipMove.
17 |
18 |
19 | ### DOM Manipulation
20 |
21 | There's no getting around it: when you practice the FLIP technique, you have to get down and dirty with the DOM.
22 |
23 | I've tried to isolate all DOM activity to a set of impure functions, located in `dom-manipulation.js`.
24 |
25 |
26 | ## FlipMove
27 |
28 | All of the core logic lives within the primary FlipMove component. It's become a bit of a behemoth, so let's walk through how it works at a high level.
29 |
30 |
31 | ### Instantiation
32 |
33 | A bunch of stuff happens when a FlipMove component is instantiated.
34 |
35 | ##### `childrenData`
36 |
37 | `childrenData` holds metadata about the rendered children. A snapshot might look like this:
38 |
39 | ```js
40 | {
41 | abc123: {
42 | domNode:
...
,
43 | boundingBox: {
44 | top: 10,
45 | left: 0,
46 | right: 500,
47 | bottom: 530,
48 | width: 100,
49 | height: 300,
50 | },
51 | },
52 | }
53 | ```
54 |
55 | The object is keyed by the `key` prop that you must supply when passing children to FlipMove, and it holds a reference to the backing instance, and that backing instance's bounding box✱.
56 |
57 | > ✱ Sidenote: Typically, a boundingBox refers to where an element is relative to the _viewport_. In our case, though, it refers to the element's position relative to its parent. We do this so that scrolling doesn't break everything.
58 |
59 | ##### `parentData`
60 |
61 | Similarly, `parentData` holds the same information about the wrapping container element, the one created by FlipMove itself.
62 |
63 |
64 | ##### `heightPlaceholderData`
65 |
66 | The default behaviour, when items are removed from the DOM, is for the container height to instantly snap down to its new height.
67 |
68 | This makes sense, when you think about it. Items that are animating out NEED to be removed from the DOM flow, so that its siblings can begin to move and take its space. However, it means that the parent container suddenly has "holes", and will collapse to only fit the non-removing containers.
69 |
70 | To combat this issue, an optional prop can be provided, to maintain the container's height until all animations have completed.
71 |
72 | In order to accomplish this goal, we have a placeholder. When items are removed, it grows to fill the "holes" created by them, so that the parent container doesn't need to shrink at all.
73 |
74 | `heightPlaceholderData` just holds a reference to that DOM node.
75 |
76 |
77 | ##### `state.children`
78 |
79 | Also within our constructor, we transfer the contents of `this.props.children` into the component's state.
80 |
81 | You may have read that this is an anti-pattern, and produces multiple sources of truth. In our case, though, we actually need two separate sources of truth, to deal with leave animations.
82 |
83 | Let's say our component receives new props, and two of the children are missing in them. We can deduce that these children need to be removed from the DOM.
84 |
85 | If we simply use this.props.children, though, the missing children will instantly disappear. We can't smoothly transition them away, because they're immediately removed from the DOM.
86 |
87 | By copying props to state, and rendering from `this.state.children`, we can hang onto them for a brief moment.
88 |
89 |
90 | ##### `remainingAnimations` and `childrenToAnimate`
91 |
92 | We need to track which element(s) are currently undergoing an animation, and how many are left.
93 |
94 | We have hooks for `onStartAll` and `onFinishAll`, and these hooks are provided the element(s) and DOM node(s) of all animating children. `childrenToAnimate` is an array which holds all the `key`s of children that are animating.
95 |
96 | `remainingAnimations` is a simple counter, and it's how we can tell that _all_ animations have completed. Because we can stagger animations, they don't all finish at the same time.
97 |
98 |
99 | ##### `originalDomStyles`
100 |
101 | Finally, in our constructor, we have an object that holds CSS.
102 |
103 | Don't worry too much about this; it's an edge-case bug fix for when items are removed and then re-introduced in rapid succession, to ensure that their original styles can be re-applied.
104 |
105 |
106 |
107 | ## FLIP flow
108 |
109 | Let's quickly go over how the FLIP process works:
110 |
111 | - The component receives props.
112 |
113 | - We update our cached values for children and parent bounding boxes. This will become the "First" position, our origins.
114 |
115 | - We compute our new set of children - this may be a simple matter of using this.props.children, but if items are leaving, we need to do some rejigging.
116 |
117 | - we set our state to these new children, causing a re-render. The re-render causes the children to render in their new, final position, also known as the 'Last' position, in FLIP terminology.
118 |
119 | - After the component has been re-rendered, but _before_ the changes have been painted to screen, we need to run our animation. Before we can do that, though, we need to do our animation prep. Prep consists of:
120 | - for children that are about to leave, remove them from the document flow, so that its siblings can shift into its position.
121 | - update the placeholder height, if needed, to keep the container open despite the removal from document flow.
122 |
123 | - Finally, the animation! We filter out all children that don't need to be animated, invoke the `onStartAll` callback with the dynamic children, and hand each one to our `animateChild` method.
124 |
125 | - `animateChild` does the actual flipping. For items that are being shuffled, it starts by calculating where it should be, by comparing the cached boundingBox with a freshly-calculated one. For items that are entering/leaving, we just merge in the `from` animation style. This is the 'Invert' stage.
126 |
127 | - At this point, the DOM has been redrawn with the items in their new positions, but then we've offset them (using `transform: translate`) so that everything is exactly where it was before the change. We allow a frame to pass, so that these invisible changes can be painted to the screen.
128 |
129 | - Then, to "Play" the animation, we simply remove our `transform` prop, and apply a `transition` so that it happens gradually. The item's transform will undo itself, and the item will shift back into its natural, new position.
130 |
131 | - We bind a `transitionEnd` event listener so that we know exactly when each animation ends. At that point, we do a few things:
132 |
133 | - remove the `transition` property we applied.
134 | - trigger the `onFinish` hook for this element.
135 | - If this is the last item that needed to animate, trigger the `onFinishAll` hook, and clean up our various variables so that the next run starts from a clean slate.
136 |
137 |
138 | ### Method Map
139 |
140 | The summary above is well and good, but sometimes I just need to refresh my memory on how the method calls are laid out. Here's what an update cycle looks like:
141 |
142 |
143 | ```
144 | - componentWillReceiveProps
145 | - this.updateBoundingBoxCaches
146 | - getRelativeBoundingBox
147 | - this.calculateNextSetOfChildren
148 | - this.setState
149 |
150 | - componentDidUpdate
151 | - this.prepForAnimation
152 | - removeNodeFromDOMFlow
153 | - updateHeightPlaceholder
154 | - this.runAnimation
155 | - this.doesChildNeedToBeAnimated
156 | - getPositionDelta
157 | - this.animateChild
158 | - this.computeInitialStyles
159 | - applyStylesToDOMNode
160 | - createTransitionString
161 | - applyStylesToDOMNode
162 | - this.bindTransitionEndHandler
163 | - this.triggerFinishHooks
164 | - this.formatChildrenForHooks
165 | - this.formatChildrenForHooks
166 | ```
167 |
--------------------------------------------------------------------------------
/src/dom-manipulation.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /**
3 | * React Flip Move
4 | * (c) 2016-present Joshua Comeau
5 | *
6 | * These methods read from and write to the DOM.
7 | * They almost always have side effects, and will hopefully become the
8 | * only spot in the codebase with impure functions.
9 | */
10 | import { findDOMNode } from 'react-dom';
11 | import type { ElementRef } from 'react';
12 |
13 | import { find, hyphenate } from './helpers';
14 | import type {
15 | Styles,
16 | ClientRect,
17 | GetPosition,
18 | NodeData,
19 | VerticalAlignment,
20 | ConvertedProps,
21 | } from './typings';
22 |
23 | export function applyStylesToDOMNode({
24 | domNode,
25 | styles,
26 | }: {
27 | domNode: HTMLElement,
28 | styles: Styles,
29 | }) {
30 | // Can't just do an object merge because domNode.styles is no regular object.
31 | // Need to do it this way for the engine to fire its `set` listeners.
32 | Object.keys(styles).forEach(key => {
33 | domNode.style.setProperty(hyphenate(key), styles[key]);
34 | });
35 | }
36 |
37 | // Modified from Modernizr
38 | export function whichTransitionEvent(): string {
39 | const transitions = {
40 | transition: 'transitionend',
41 | '-o-transition': 'oTransitionEnd',
42 | '-moz-transition': 'transitionend',
43 | '-webkit-transition': 'webkitTransitionEnd',
44 | };
45 |
46 | // If we're running in a browserless environment (eg. SSR), it doesn't apply.
47 | // Return a placeholder string, for consistent type return.
48 | if (typeof document === 'undefined') return '';
49 |
50 | const el = document.createElement('fakeelement');
51 |
52 | const match = find(
53 | t => el.style.getPropertyValue(t) !== undefined,
54 | Object.keys(transitions),
55 | );
56 |
57 | // If no `transition` is found, we must be running in a browser so ancient,
58 | // React itself won't run. Return an empty string, for consistent type return
59 | return match ? transitions[match] : '';
60 | }
61 |
62 | export const getRelativeBoundingBox = ({
63 | childDomNode,
64 | parentDomNode,
65 | getPosition,
66 | }: {
67 | childDomNode: HTMLElement,
68 | parentDomNode: HTMLElement,
69 | getPosition: GetPosition,
70 | }): ClientRect => {
71 | const parentBox = getPosition(parentDomNode);
72 | const { top, left, right, bottom, width, height } = getPosition(childDomNode);
73 |
74 | return {
75 | top: top - parentBox.top,
76 | left: left - parentBox.left,
77 | right: parentBox.right - right,
78 | bottom: parentBox.bottom - bottom,
79 | width,
80 | height,
81 | };
82 | };
83 |
84 | /** getPositionDelta
85 | * This method returns the delta between two bounding boxes, to figure out
86 | * how many pixels on each axis the element has moved.
87 | *
88 | */
89 | export const getPositionDelta = ({
90 | childDomNode,
91 | childBoundingBox,
92 | parentBoundingBox,
93 | getPosition,
94 | }: {
95 | childDomNode: HTMLElement,
96 | childBoundingBox: ?ClientRect,
97 | parentBoundingBox: ?ClientRect,
98 | getPosition: GetPosition,
99 | }): [number, number] => {
100 | // TEMP: A mystery bug is sometimes causing unnecessary boundingBoxes to
101 | // remain. Until this bug can be solved, this band-aid fix does the job:
102 | const defaultBox: ClientRect = {
103 | top: 0,
104 | left: 0,
105 | right: 0,
106 | bottom: 0,
107 | height: 0,
108 | width: 0,
109 | };
110 |
111 | // Our old box is its last calculated position, derived on mount or at the
112 | // start of the previous animation.
113 | const oldRelativeBox = childBoundingBox || defaultBox;
114 | const parentBox = parentBoundingBox || defaultBox;
115 |
116 | // Our new box is the new final resting place: Where we expect it to wind up
117 | // after the animation. First we get the box in absolute terms (AKA relative
118 | // to the viewport), and then we calculate its relative box (relative to the
119 | // parent container)
120 | const newAbsoluteBox = getPosition(childDomNode);
121 | const newRelativeBox = {
122 | top: newAbsoluteBox.top - parentBox.top,
123 | left: newAbsoluteBox.left - parentBox.left,
124 | };
125 |
126 | return [
127 | oldRelativeBox.left - newRelativeBox.left,
128 | oldRelativeBox.top - newRelativeBox.top,
129 | ];
130 | };
131 |
132 | /** removeNodeFromDOMFlow
133 | * This method does something very sneaky: it removes a DOM node from the
134 | * document flow, but without actually changing its on-screen position.
135 | *
136 | * It works by calculating where the node is, and then applying styles
137 | * so that it winds up being positioned absolutely, but in exactly the
138 | * same place.
139 | *
140 | * This is a vital part of the FLIP technique.
141 | */
142 | export const removeNodeFromDOMFlow = (
143 | childData: NodeData,
144 | verticalAlignment: VerticalAlignment,
145 | ) => {
146 | const { domNode, boundingBox } = childData;
147 |
148 | if (!domNode || !boundingBox) {
149 | return;
150 | }
151 |
152 | // For this to work, we have to offset any given `margin`.
153 | const computed: CSSStyleDeclaration = window.getComputedStyle(domNode);
154 |
155 | // We need to clean up margins, by converting and removing suffix:
156 | // eg. '21px' -> 21
157 | const marginAttrs = ['margin-top', 'margin-left', 'margin-right'];
158 | const margins: {
159 | [string]: number,
160 | } = marginAttrs.reduce((acc, margin) => {
161 | const propertyVal = computed.getPropertyValue(margin);
162 |
163 | return {
164 | ...acc,
165 | [margin]: Number(propertyVal.replace('px', '')),
166 | };
167 | }, {});
168 |
169 | // If we're bottom-aligned, we need to add the height of the child to its
170 | // top offset. This is because, when the container is bottom-aligned, its
171 | // height shrinks from the top, not the bottom. We're removing this node
172 | // from the flow, so the top is going to drop by its height.
173 | const topOffset =
174 | verticalAlignment === 'bottom'
175 | ? boundingBox.top - boundingBox.height
176 | : boundingBox.top;
177 |
178 | const styles: Styles = {
179 | position: 'absolute',
180 | top: `${topOffset - margins['margin-top']}px`,
181 | left: `${boundingBox.left - margins['margin-left']}px`,
182 | right: `${boundingBox.right - margins['margin-right']}px`,
183 | };
184 |
185 | applyStylesToDOMNode({ domNode, styles });
186 | };
187 |
188 | /** updateHeightPlaceholder
189 | * An optional property to FlipMove is a `maintainContainerHeight` boolean.
190 | * This property creates a node that fills space, so that the parent
191 | * container doesn't collapse when its children are removed from the
192 | * document flow.
193 | */
194 | export const updateHeightPlaceholder = ({
195 | domNode,
196 | parentData,
197 | getPosition,
198 | }: {
199 | domNode: HTMLElement,
200 | parentData: NodeData,
201 | getPosition: GetPosition,
202 | }) => {
203 | const parentDomNode = parentData.domNode;
204 | const parentBoundingBox = parentData.boundingBox;
205 |
206 | if (!parentDomNode || !parentBoundingBox) {
207 | return;
208 | }
209 |
210 | // We need to find the height of the container *without* the placeholder.
211 | // Since it's possible that the placeholder might already be present,
212 | // we first set its height to 0.
213 | // This allows the container to collapse down to the size of just its
214 | // content (plus container padding or borders if any).
215 | applyStylesToDOMNode({ domNode, styles: { height: '0' } });
216 |
217 | // Find the distance by which the container would be collapsed by elements
218 | // leaving. We compare the freshly-available parent height with the original,
219 | // cached container height.
220 | const originalParentHeight = parentBoundingBox.height;
221 | const collapsedParentHeight = getPosition(parentDomNode).height;
222 | const reductionInHeight = originalParentHeight - collapsedParentHeight;
223 |
224 | // If the container has become shorter, update the padding element's
225 | // height to take up the difference. Otherwise set its height to zero,
226 | // so that it has no effect.
227 | const styles: Styles = {
228 | height: reductionInHeight > 0 ? `${reductionInHeight}px` : '0',
229 | };
230 |
231 | applyStylesToDOMNode({ domNode, styles });
232 | };
233 |
234 | export const getNativeNode = (element: ElementRef<*>): ?HTMLElement => {
235 | // When running in a windowless environment, abort!
236 | if (typeof HTMLElement === 'undefined') {
237 | return null;
238 | }
239 |
240 | // `element` may already be a native node.
241 | if (element instanceof HTMLElement) {
242 | return element;
243 | }
244 |
245 | // While ReactDOM's `findDOMNode` is discouraged, it's the only
246 | // publicly-exposed way to find the underlying DOM node for
247 | // composite components.
248 | const foundNode: ?(Element | Text) = findDOMNode(element);
249 |
250 | if (foundNode && foundNode.nodeType === Node.TEXT_NODE) {
251 | // Text nodes are not supported
252 | return null;
253 | }
254 | // eslint-disable-next-line flowtype/no-weak-types
255 | return ((foundNode: any): ?HTMLElement);
256 | };
257 |
258 | export const createTransitionString = (
259 | index: number,
260 | props: ConvertedProps,
261 | ): string => {
262 | let { delay, duration } = props;
263 | const { staggerDurationBy, staggerDelayBy, easing } = props;
264 |
265 | delay += index * staggerDelayBy;
266 | duration += index * staggerDurationBy;
267 |
268 | const cssProperties = ['transform', 'opacity'];
269 |
270 | return cssProperties
271 | .map(prop => `${prop} ${duration}ms ${easing} ${delay}ms`)
272 | .join(', ');
273 | };
274 |
--------------------------------------------------------------------------------
/stories/legacy.stories.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/forbid-prop-types */
2 | import React, { Component } from 'react';
3 | import { storiesOf } from '@storybook/react';
4 | import shuffle from 'lodash/shuffle';
5 | import range from 'lodash/range';
6 | import PropTypes from 'prop-types';
7 |
8 | import FlipMove from '../src';
9 |
10 | const getPosition = node =>
11 | Object.values(node.getBoundingClientRect()).reduce(
12 | (newRect, prop) => ({ ...newRect, [prop]: prop / 0.5 }),
13 | {},
14 | );
15 |
16 | storiesOf('Legacy Stories', module)
17 | .add('simple transition', () => )
18 | .add('when animation is disabled', () => )
19 | .add('when removing items - elevator (default)', () => (
20 |
21 | ))
22 | .add('when removing items - fade', () => (
23 |
24 | ))
25 | .add('when removing items - accordionVertical', () => (
26 |
31 | ))
32 | .add('when removing items - accordionHorizontal', () => (
33 |
38 | ))
39 | .add('when removing items - none', () => (
40 |
41 | ))
42 | .add(
43 | 'when adding/removing items - custom object with default "leave"',
44 | () => (
45 |
56 | ),
57 | )
58 | .add('when adding/removing items - custom object with rotate', () => (
59 |
70 | ))
71 | .add('when adding/removing items - custom object with 3D rotate', () => (
72 |
83 | ))
84 | .add('with centered flex content', () => (
85 |
86 | ))
87 | .add('with transition on child', () => (
88 |
95 | ))
96 | .add('with onStartAll callback', () => (
97 |
99 | // eslint-disable-next-line no-console
100 | console.log('Started with', elements, nodes)
101 | }
102 | />
103 | ))
104 | .add('when prop keys do not change, but items rearrange', () => (
105 |
106 | ))
107 | .add('delegated prop - width', () => (
108 |
109 | ))
110 | .add('inside a scaled container', () => (
111 |
112 | ))
113 | .add('empty', () => {
114 | class HandleEmpty extends Component {
115 | constructor(props) {
116 | super(props);
117 |
118 | this.state = {
119 | empty: true,
120 | };
121 | }
122 |
123 | render() {
124 | return (
125 |
332 | );
333 | }
334 | }
335 |
--------------------------------------------------------------------------------
/typings/react-flip-move.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for react-flip-move 2.9
2 | // Project: https://github.com/joshwcomeau/react-flip-move
3 | // Definitions by: Joey Hain ,
4 | // Ville Saarinen
5 | // TypeScript Version: 2.3
6 |
7 | import { Component, ReactElement } from 'react';
8 |
9 | export as namespace FlipMove;
10 | export default FlipMove;
11 |
12 | declare class FlipMove extends Component { }
13 |
14 | declare namespace FlipMove {
15 | type AnimationPreset = 'elevator' | 'fade' | 'accordionVertical' | 'accordionHorizontal' | 'none';
16 |
17 | interface Animation {
18 | from: Partial;
19 | to: Partial;
20 | }
21 |
22 | interface Styles {
23 | [key: string]: string | number;
24 | }
25 |
26 | type AnimationProp = AnimationPreset | Animation | boolean;
27 |
28 | interface FlipMoveProps {
29 | /**
30 | * The children to animate. Each child must have a unique key to work correctly.
31 | */
32 | children?: React.ReactNode;
33 |
34 | className?: string;
35 |
36 | /**
37 | * Any valid CSS3 timing function (eg. "linear", "ease-in", "cubic-bezier(1, 0, 0, 1)").
38 | *
39 | * @default "ease-in-out"
40 | */
41 | easing?: string;
42 |
43 | /**
44 | * The length, in milliseconds, that the transition ought to take.
45 | *
46 | * @default 350
47 | */
48 | duration?: number | string;
49 |
50 | /**
51 | * The length, in milliseconds, to wait before the animation begins.
52 | *
53 | * @default 0
54 | */
55 | delay?: number | string;
56 |
57 | /**
58 | * The length, in milliseconds, to be added to the duration of each subsequent element.
59 | *
60 | * For example, if you are animating 4 elements with a `duration` of 200 and a `staggerDurationBy` of 20:
61 | *
62 | * - The first element will take 200ms to transition.
63 | * - The second element will take 220ms to transition.
64 | * - The third element will take 240ms to transition.
65 | * - The fourth element will take 260ms to transition.
66 | *
67 | * This effect is great for "humanizing" transitions and making them feel less robotic.
68 | *
69 | * @default 0
70 | */
71 | staggerDurationBy?: number | string;
72 |
73 | /**
74 | * The length, in milliseconds, to be added to the delay of each subsequent element.
75 | *
76 | * For example, if you are animating 4 elements with a `delay` of 0 and a `staggerDelayBy` of 20:
77 | *
78 | * - The first element will start transitioning immediately.
79 | * - The second element will start transitioning after 20ms.
80 | * - The third element will start transitioning after 40ms.
81 | * - The fourth element will start transitioning after 60ms.
82 | *
83 | * Similarly to staggerDurationBy, This effect is great for "humanizing" transitions and making them feel less
84 | * robotic.
85 | *
86 | * **Protip**: You can make elements animate one-at-a-time by using an identical duration and staggerDelayBy.
87 | *
88 | * @default 0
89 | */
90 | staggerDelayBy?: number | string;
91 |
92 | /**
93 | * Control the appear animation that runs when the component mounts. Works identically to enterAnimation below, but
94 | * only fires on the initial children.
95 | */
96 | appearAnimation?: AnimationProp;
97 |
98 | /**
99 | * Control the onEnter animation that runs when new items are added to the DOM.
100 | *
101 | * Accepts several types:
102 | *
103 | * **String:** You can enter one of the following presets to select that as your enter animation:
104 | *
105 | * - `elevator` (default)
106 | * - `fade`
107 | * - `accordionVertical`
108 | * - `accordionHorizontal`
109 | * - `none`
110 | *
111 | * **Boolean:** You can enter `false` to disable the enter animation, or `true` to select the default enter animation (elevator).
112 | *
113 | * **Object:** For fully granular control, you can pass in an object that contains the styles you'd like to animate.
114 | *
115 | * It requires two keys: `from` and `to`. Each key holds an object of CSS properties. You can supply any valid
116 | * camelCase CSS properties, and flip-move will transition between the two, over the course of the specified
117 | * `duration`.
118 | *
119 | * Example:
120 | *
121 | * const customEnterAnimation = {
122 | * from: { transform: 'scale(0.5, 1)' },
123 | * to: { transform: 'scale(1, 1)' }
124 | * };
125 | *
126 | *
127 | * {renderChildren()}
128 | *
129 | *
130 | * @default "elevator"
131 | */
132 | enterAnimation?: AnimationProp;
133 |
134 | /**
135 | * Control the onLeave animation that runs when new items are removed from the DOM.
136 | *
137 | * This property functions identically to `enterAnimation`.
138 | *
139 | * @default "elevator"
140 | */
141 | leaveAnimation?: AnimationProp;
142 |
143 | /**
144 | * Do not collapse container height until after leaving animations complete.
145 | *
146 | * When false, children are immediately removed from the DOM flow as they animate away. Setting this value to true
147 | * will maintain the height of the container until after their leaving animation completes.
148 | *
149 | * @default false
150 | */
151 | maintainContainerHeight?: boolean;
152 |
153 | /**
154 | * A callback to be invoked **once per child element** at the start of the animation.
155 | *
156 | * In general, it is advisable to ignore the domNode argument and work with the childElement. The domNode is just
157 | * an escape hatch for doing complex things not otherwise possible.
158 | *
159 | * @param childElement A reference to the React Element being animated.
160 | * @param domNode A reference to the unadulterated DOM node being animated.
161 | */
162 | onStart?(childElement: ReactElement, domNode: HTMLElement): void;
163 |
164 | /**
165 | * A callback to be invoked **once per child element** at the end of the animation.
166 | *
167 | * In general, it is advisable to ignore the `domNode` argument and work with the `childElement`. The `domNode` is
168 | * just an escape hatch for doing complex things not otherwise possible.
169 | *
170 | * @param childElement A reference to the React Element being animated.
171 | * @param domNode A reference to the unadulterated DOM node being animated.
172 | */
173 | onFinish?(childElement: ReactElement, domNode: HTMLElement): void;
174 |
175 | /**
176 | * A callback to be invoked **once per group** at the start of the animation.
177 | *
178 | * The callback arguments are similar to the ones provided for onStart, except we provide an array of the elements
179 | * and nodes. The order of both arguments is guaranteed; this means you can use a zipping function like lodash's
180 | * .zip to get pairs of element/node, if needed.
181 | *
182 | * In general, it is advisable to ignore the `domNodes` argument and work with the `childElements` The `domNodes`
183 | *
184 | * are just an escape hatch for doing complex things not otherwise possible.
185 | *
186 | * @param childElements An array of the references to the React Element(s) being animated.
187 | * @param domNodes An array of the references to the unadulterated DOM node(s) being animated.
188 | */
189 | onStartAll?(childElements: Array>, domNodes: Array): void;
190 |
191 | /**
192 | * A callback to be invoked **once per group** at the end of the animation.
193 | *
194 | * The callback arguments are similar to the ones provided for onFinish, except we provide an array of the elements
195 | * and nodes. The order of both arguments is guaranteed; this means you can use a zipping function like lodash's
196 | * .zip to get pairs of element/node, if needed.
197 | *
198 | * In general, it is advisable to ignore the `domNodes` argument and work with the `childElements` The `domNodes`
199 | * are just an escape hatch for doing complex things not otherwise possible.
200 | *
201 | * @param childElements An array of the references to the React Element(s) being animated.
202 | * @param domNodes An array of the references to the unadulterated DOM node(s) being animated.
203 | */
204 | onFinishAll?(childElements: Array>, domNodes: Array): void;
205 |
206 | /**
207 | * Flip Move wraps your children in a container element. By default, this element is a div, but you may wish to
208 | * provide a custom HTML element (for example, if your children are list items, you may wish to set this to ul).
209 | *
210 | * Any valid HTML element type is accepted, but peculiar things may happen if you use an unconventional element.
211 | *
212 | * @default "div"
213 | */
214 | typeName?: string | null;
215 |
216 | /**
217 | * Sometimes, you may wish to temporarily disable the animations and have the normal behaviour resumed. Setting this
218 | * flag to true skips all animations.
219 | *
220 | * @default false
221 | */
222 | disableAllAnimations?: boolean;
223 |
224 | /**
225 | * This function is called with a DOM node as the only argument. It should return an object as specified by the
226 | * getBoundingClientRect() spec.
227 | *
228 | * For normal usage of FlipMove you won't need this. An example of usage is when FlipMove is used in a container
229 | * that is scaled using CSS. You can correct the values from getBoundingClientRect by using this prop.
230 | */
231 | getPosition?(node: HTMLElement): ClientRect;
232 |
233 | /**
234 | * @default "top"
235 | */
236 | verticalAlignment?: string;
237 |
238 | style?: Styles;
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/documentation/api_reference.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | FlipMove is a React component, and is configured via the following props:
4 |
5 |
6 |
7 | ### `children`
8 |
9 | | **Accepted Types:** | **Default Value** |
10 | |---------------------|-------------------|
11 | | `Array`, `Object` | `undefined` |
12 |
13 |
14 | The children passed to FlipMove are the component(s) or DOM element(s) that will be moved about. Accepts either a single child (as long as it has a unique `key` property) or an array of children.
15 |
16 | ---
17 |
18 | ### `easing`
19 |
20 | | **Accepted Types:** | **Default Value** |
21 | |---------------------|-------------------|
22 | | `String` | "ease-in-out" |
23 |
24 |
25 | Any valid CSS3 timing function (eg. "linear", "ease-in", "cubic-bezier(1, 0, 0, 1)").
26 |
27 | ---
28 |
29 | ### `duration`
30 |
31 | | **Accepted Types:** | **Default Value** |
32 | |---------------------|-------------------|
33 | | `Number` | `350` |
34 |
35 |
36 | The length, in milliseconds, that the transition ought to take.
37 |
38 |
39 | ---
40 |
41 | ### `delay`
42 |
43 | | **Accepted Types:** | **Default Value** |
44 | |---------------------|-------------------|
45 | | `Number` | `0` |
46 |
47 |
48 | The length, in milliseconds, to wait before the animation begins.
49 |
50 | ---
51 |
52 | ### `staggerDurationBy`
53 |
54 | | **Accepted Types:** | **Default Value** |
55 | |---------------------|-------------------|
56 | | `Number` | `0` |
57 |
58 |
59 | The length, in milliseconds, to be added to the duration of each subsequent element.
60 |
61 | For example, if you are animating 4 elements with a `duration` of 200 and a `staggerDurationBy` of 20:
62 |
63 | * The first element will take 200ms to transition.
64 | * The second element will take 220ms to transition.
65 | * The third element will take 240ms to transition.
66 | * The fourth element will take 260ms to transition.
67 |
68 | This effect is great for "humanizing" transitions and making them feel less robotic.
69 |
70 | ---
71 |
72 | ### `staggerDelayBy`
73 |
74 | | **Accepted Types:** | **Default Value** |
75 | |---------------------|-------------------|
76 | | `Number` | `0` |
77 |
78 |
79 | The length, in milliseconds, to be added to the delay of each subsequent element.
80 |
81 | For example, if you are animating 4 elements with a `delay` of 0 and a `staggerDelayBy` of 20:
82 |
83 | * The first element will start transitioning immediately.
84 | * The second element will start transitioning after 20ms.
85 | * The third element will start transitioning after 40ms.
86 | * The fourth element will start transitioning after 60ms.
87 |
88 | Similarly to staggerDurationBy, This effect is great for "humanizing" transitions and making them feel less robotic.
89 |
90 | **Protip:** You can make elements animate one-at-a-time by using an identical `duration` and `staggerDelayBy`.
91 |
92 | ---
93 |
94 | ### `appearAnimation`
95 |
96 | | **Accepted Types:** | **Default Value** |
97 | |--------------------------------|-------------------|
98 | | `String`, `Boolean`, `Object` | undefined |
99 |
100 | Control the appear animation that runs when the component mounts. Works identically to [`enterAnimation`](#enteranimation) below, but only fires on the initial children.
101 |
102 | ---
103 |
104 | ### `enterAnimation`
105 |
106 | | **Accepted Types:** | **Default Value** |
107 | |--------------------------------|-------------------|
108 | | `String`, `Boolean`, `Object` | 'elevator' |
109 |
110 | Control the onEnter animation that runs when new items are added to the DOM. For examples of this property, see the enter/leave docs.
111 |
112 | Accepts several types:
113 |
114 | **String:** You can enter one of the following presets to select that as your enter animation:
115 | * `elevator` (default)
116 | * `fade`
117 | * `accordionVertical`
118 | * `accordionHorizontal`
119 | * `none`
120 |
121 | View the CSS implementation of these presets.
122 |
123 | **Boolean:** You can enter `false` to disable the enter animation, or `true` to select the default enter animation (elevator).
124 |
125 | **Object:** For fully granular control, you can pass in an object that contains the styles you'd like to animate.
126 |
127 | It requires two keys: `from` and `to`. Each key holds an object of CSS properties. You can supply any valid camelCase CSS properties, and flip-move will transition between the two, over the course of the specified `duration`.
128 |
129 | Example:
130 |
131 | ```jsx
132 | const customEnterAnimation = {
133 | from: { transform: 'scale(0.5, 1)' },
134 | to: { transform: 'scale(1, 1)' }
135 | };
136 |
137 |
138 | {renderChildren()}
139 |
140 | ```
141 |
142 | It is recommended that you stick to hardware-accelerated CSS properties for optimal performance: transform and opacity.
143 |
144 | ---
145 |
146 | ### `leaveAnimation`
147 |
148 | | **Accepted Types:** | **Default Value** |
149 | |--------------------------------|-------------------|
150 | | `String`, `Boolean`, `Object` | 'elevator' |
151 |
152 | Control the onLeave animation that runs when new items are removed from the DOM. For examples of this property, see the enter/leave docs.
153 |
154 | This property functions identically to `enterAnimation`.
155 |
156 | Accepts several types:
157 |
158 | **String:** You can enter one of the following presets to select that as your enter animation:
159 | * `elevator` (default)
160 | * `fade`
161 | * `accordionVertical`
162 | * `accordionHorizontal`
163 | * `none`
164 |
165 | View the CSS implementation of these presets.
166 |
167 | **Boolean:** You can enter `false` to disable the leave animation, or `true` to select the default leave animation (elevator).
168 |
169 | **Object:** For fully granular control, you can pass in an object that contains the styles you'd like to animate.
170 |
171 | It requires two keys: `from` and `to`. Each key holds an object of CSS properties. You can supply any valid camelCase CSS properties, and flip-move will transition between the two, over the course of the specified `duration`.
172 |
173 | Example:
174 |
175 | ```jsx
176 | const customLeaveAnimation = {
177 | from: { transform: 'scale(1, 1)' },
178 | to: { transform: 'scale(0.5, 1) translateY(-20px)' }
179 | };
180 |
181 |
182 | {renderChildren()}
183 |
184 | ```
185 |
186 | It is recommended that you stick to hardware-accelerated CSS properties for optimal performance: transform and opacity.
187 |
188 | ---
189 |
190 | ### `maintainContainerHeight`
191 |
192 | | **Accepted Types:** | **Default Value** |
193 | |---------------------|-------------------|
194 | | `Boolean` | `false` |
195 |
196 | Do not collapse container height until after leaving animations complete.
197 |
198 | When `false`, children are immediately removed from the DOM flow as they animate away. Setting this value to `true` will maintain the height of the container until after their leaving animation completes.
199 |
200 | ---
201 |
202 | ### `verticalAlignment`
203 |
204 | | **Accepted Types:** | **Default Value** | **Accepted Values** |
205 | |---------------------|-------------------|---------------------|
206 | | `String` | `'top'` | `'top'`, `'bottom'` |
207 |
208 | If the container is bottom-aligned and an element is removed, the container's top edge moves lower. You can tell `react-flip-move` to account for this by passing `'bottom'` to the `verticalAlignment` prop.
209 |
210 | ---
211 |
212 | ### `onStart`
213 |
214 | | **Accepted Types:** | **Default Value** |
215 | |---------------------|-------------------|
216 | | `Function` | `undefined` |
217 |
218 |
219 | A callback to be invoked **once per child element** at the start of the animation.
220 |
221 | The callback is invoked with two arguments:
222 |
223 | * `childElement`: A reference to the React Element being animated.
224 | * `domNode`: A reference to the unadulterated DOM node being animated.
225 |
226 | In general, it is advisable to ignore the `domNode` argument and work with the `childElement`. The `domNode` is just an escape hatch for doing complex things not otherwise possible.
227 |
228 | ---
229 |
230 | ### `onFinish`
231 |
232 | | **Accepted Types:** | **Default Value** |
233 | |---------------------|-------------------|
234 | | `Function` | `undefined` |
235 |
236 |
237 | A callback to be invoked **once per child element** at the end of the animation.
238 |
239 | The callback is invoked with two arguments:
240 |
241 | * `childElement`: A reference to the React Element being animated.
242 | * `domNode`: A reference to the unadulterated DOM node being animated.
243 |
244 | In general, it is advisable to ignore the `domNode` argument and work with the `childElement`. The `domNode` is just an escape hatch for doing complex things not otherwise possible.
245 |
246 | ---
247 |
248 | ### `onStartAll`
249 |
250 | | **Accepted Types:** | **Default Value** |
251 | |---------------------|-------------------|
252 | | `Function` | `undefined` |
253 |
254 |
255 | A callback to be invoked **once per group** at the start of the animation.
256 |
257 | The callback is invoked with two arguments:
258 |
259 | * `childElements`: An array of the references to the React Element(s) being animated.
260 | * `domNodes`: An array of the references to the unadulterated DOM node(s) being animated.
261 |
262 | These arguments are similar to the ones provided for `onStart`, except we provide an *array* of the elements and nodes. The order of both arguments is guaranteed; this means you can use a zipping function like lodash's .zip to get pairs of element/node, if needed.
263 |
264 | In general, it is advisable to ignore the `domNodes` argument and work with the `childElements`. The `domNodes` are just an escape hatch for doing complex things not otherwise possible.
265 |
266 | ---
267 |
268 | ### `onFinishAll`
269 |
270 | | **Accepted Types:** | **Default Value** |
271 | |---------------------|-------------------|
272 | | `Function` | `undefined` |
273 |
274 |
275 | A callback to be invoked **once per group** at the end of the animation.
276 |
277 | The callback is invoked with two arguments:
278 |
279 | * `childElements`: An array of the references to the React Element(s) being animated.
280 | * `domNodes`: An array of the references to the unadulterated DOM node(s) being animated.
281 |
282 | These arguments are similar to the ones provided for `onFinish`, except we provide an *array* of the elements and nodes. The order of both arguments is guaranteed; this means you can use a zipping function like lodash's .zip to get pairs of element/node, if needed.
283 |
284 | In general, it is advisable to ignore the `domNodes` argument and work with the `childElements`. The `domNodes` are just an escape hatch for doing complex things not otherwise possible.
285 |
286 | ---
287 |
288 | ### `typeName`
289 |
290 | | **Accepted Types:** | **Default Value** |
291 | |----------------------|-------------------|
292 | | `String`, `null` | 'div' |
293 |
294 |
295 | Flip Move wraps your children in a container element. By default, this element is a `div`, but you may wish to provide a custom HTML element (for example, if your children are list items, you may wish to set this to `ul`).
296 |
297 | Any valid HTML element type is accepted, but peculiar things may happen if you use an unconventional element.
298 |
299 | With React 16, Flip Move can opt not to use a container element: set `typeName` to `null` to use this new "wrapperless" behaviour. [Read more](https://github.com/joshwcomeau/react-flip-move/blob/master/README.md#wrapping-elements).
300 |
301 | ---
302 |
303 | ### `disableAllAnimations`
304 |
305 | | **Accepted Types:** | **Default Value** |
306 | |---------------------|-------------------|
307 | | `Boolean` | `false` |
308 |
309 |
310 | Sometimes, you may wish to temporarily disable the animations and have the normal behaviour resumed. Setting this flag to `true` skips all animations.
311 |
312 | ---
313 |
314 | ### `getPosition`
315 |
316 | | **Accepted Types:** | **Default Value** |
317 | |---------------------|-------------------------|
318 | | `Function` | `getBoundingClientRect` |
319 |
320 |
321 | This function is called with a DOM node as the only argument. It should return an object as specified by the [getBoundingClientRect() spec](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect).
322 |
323 | For normal usage of FlipMove you won't need this. An example of usage is when FlipMove is used in a container that is scaled using CSS. You can correct the values from `getBoundingClientRect` by using this prop.
324 |
325 | ---
326 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | React Flip Move
2 | =========
3 |
4 | [](https://travis-ci.org/joshwcomeau/react-flip-move)
5 | [](https://www.npmjs.com/package/react-flip-move)
6 | [](https://www.npmjs.com/package/react-flip-move)
7 |
8 |
9 |
10 | This module was built to tackle the common but arduous problem of animating a list of items when the list's order changes.
11 |
12 | CSS transitions only work for CSS properties. If your list is shuffled, the items have rearranged themselves, but without the use of CSS. The DOM nodes don't know that their on-screen location has changed; from their perspective, they've been removed and inserted elsewhere in the document.
13 |
14 | Flip Move uses the [_FLIP technique_](https://aerotwist.com/blog/flip-your-animations/#the-general-approach) to work out what such a transition would look like, and fakes it using 60+ FPS hardware-accelerated CSS transforms.
15 |
16 | [**Read more about how it works**](https://medium.com/developers-writing/animating-the-unanimatable-1346a5aab3cd)
17 |
18 | [](http://joshwcomeau.github.io/react-flip-move/examples/#/shuffle)
19 |
20 |
21 | ## Current Status
22 |
23 | React Flip Move is [looking for maintainers](https://github.com/joshwcomeau/react-flip-move/issues/233)!
24 |
25 | In the meantime, we'll do our best to make sure React Flip Move continues to work with new versions of React, but otherwise it isn't being actively worked on.
26 |
27 | Because it isn't under active development, you may be interested in checking out projects like [react-flip-toolkit](https://github.com/aholachek/react-flip-toolkit).
28 |
29 |
30 | ## Demos
31 |
32 | * __List/Grid Shuffle__
33 | * __Fuscia Square__
34 | * __Scrabble__
35 | * __Laboratory__
36 |
37 |
38 |
39 | ## Installation
40 |
41 | Flip Move can be installed with [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/en/).
42 |
43 | ```bash
44 | yarn add react-flip-move
45 |
46 | # Or, if not using yarn:
47 | npm i -S react-flip-move
48 | ```
49 |
50 | A UMD build is made available for those not using JS package managers:
51 | * [react-flip-move.js](https://unpkg.com/react-flip-move/dist/react-flip-move.js)
52 | * [react-flip-move.min.js](https://unpkg.com/react-flip-move/dist/react-flip-move.min.js)
53 |
54 | To use a UMD build, you can use `
60 |
63 |