Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

animated droppable placeholder spike #74

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"react-redux": "5.0.6",
"redux": "^3.6.0",
"redux-thunk": "2.2.0",
"reselect": "3.0.1"
"reselect": "3.0.1",
"uuid": "^3.1.0"
},
"devDependencies": {
"@atlaskit/css-reset": "1.1.5",
Expand Down
7 changes: 6 additions & 1 deletion src/view/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export const physics = (() => {
return { standard, fast };
})();

const transitionCurve = 'cubic-bezier(0.2, 0, 0, 1)';
const transitionTime = '0.2s';

export const css = {
outOfTheWay: 'transform 0.2s cubic-bezier(0.2, 0, 0, 1)',
outOfTheWay: `transform ${transitionTime} ${transitionCurve}`,
transitionCurve,
transitionTime,
};
2 changes: 1 addition & 1 deletion src/view/draggable/draggable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
Provided as DragHandleProvided,
} from '../drag-handle/drag-handle-types';
import getCenterPosition from '../get-center-position';
import Placeholder from '../placeholder';
import { StaticPlaceholder as Placeholder } from '../placeholder';
import { droppableIdKey } from '../context-keys';
import { add } from '../../state/position';
import type {
Expand Down
13 changes: 7 additions & 6 deletions src/view/droppable/connected-droppable.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,10 @@ export const makeSelector = (): Selector => {
);

const getMapProps = memoizeOne(
(isDraggingOver: boolean, placeholder: ?Placeholder): MapProps => ({
(isDraggingOver: boolean, placeholder: ?Placeholder, phase: ?string): MapProps => ({
isDraggingOver,
placeholder,
phase,
})
);

Expand All @@ -107,7 +108,7 @@ export const makeSelector = (): Selector => {
if (phase === 'DRAGGING') {
if (!drag) {
console.error('cannot determine dragging over as there is not drag');
return getMapProps(false, null);
return getMapProps(false, null, phase);
}

const isDraggingOver = getIsDraggingOver(id, drag.impact.destination);
Expand All @@ -117,13 +118,13 @@ export const makeSelector = (): Selector => {
drag.impact.destination,
draggable
);
return getMapProps(isDraggingOver, placeholder);
return getMapProps(isDraggingOver, placeholder, phase);
}

if (phase === 'DROP_ANIMATING') {
if (!pending) {
console.error('cannot determine dragging over as there is no pending result');
return getMapProps(false, null);
return getMapProps(false, null, phase);
}

const isDraggingOver = getIsDraggingOver(id, pending.impact.destination);
Expand All @@ -133,10 +134,10 @@ export const makeSelector = (): Selector => {
pending.result.destination,
draggable
);
return getMapProps(isDraggingOver, placeholder);
return getMapProps(isDraggingOver, placeholder, phase);
}

return getMapProps(false, null);
return getMapProps(false, null, phase);
},
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/view/droppable/droppable-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type MapProps = {|
// not the user is dragging over a list that
// is not the source list
placeholder: ?Placeholder,
// whether the entire application is currently in the dragging state
phase: ?string,
|}

export type OwnProps = {|
Expand Down
14 changes: 9 additions & 5 deletions src/view/droppable/droppable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type Context = {|
[droppableIdKey]: DroppableId
|}

const defaultPlaceholderDimensions = { height: 0, width: 0 };

export default class Droppable extends Component {
/* eslint-disable react/sort-comp */
props: Props
Expand Down Expand Up @@ -62,14 +64,16 @@ export default class Droppable extends Component {
}

getPlaceholder() {
if (!this.props.placeholder) {
return null;
}
const { phase } = this.props;
const isDraggedOver = Boolean(this.props.placeholder);
const { height, width } = this.props.placeholder || defaultPlaceholderDimensions;

return (
<Placeholder
height={this.props.placeholder.height}
width={this.props.placeholder.width}
phase={phase}
isDraggedOver={isDraggedOver}
height={height}
width={width}
/>
);
}
Expand Down
192 changes: 192 additions & 0 deletions src/view/placeholder/Placeholder.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// @flow
import React, { PureComponent } from 'react';
import v4 from 'uuid/v4';
import { css as transitionStyles } from '../animation';
import type { Phase } from '../../types';

type AnimatedProperties = {|
/** The height of the placeholder */
height: number,
/** The width of the placeholder */
width: number,
|};

type Props = {|
...AnimatedProperties,
/** Whether the parent droppable is being dragged over */
isDraggedOver: boolean,
/** The phase of the drag */
phase: Phase,
|};

type State = {|
...AnimatedProperties,
/** Whether the placeholder should be animating in */
isAnimatingIn: boolean,
/** Whether the placeholder should be animating out */
isAnimatingOut: boolean,
|}

const VENDOR_PREFIXES: string[] = ['-ms-', '-moz-', '-webkit-', ''];
// We inject a stylesheet into the document so that we can dynamically
// add CSS animations.
const stylesheet: HTMLStyleElement = document.createElement('style');
stylesheet.type = 'text/css';
document.getElementsByTagName('head')[0].appendChild(stylesheet);
// A global counter keeping track of how many style rules have been
// added to the stylesheet.
let globalRuleIndex = 0;

export default class Placeholder extends PureComponent {
// eslint-disable-next-line react/sort-comp
props: Props

static defaultProps = {
isDraggedOver: false,
}

state: State = {
height: this.props.height,
isAnimatingIn: false,
isAnimatingOut: false,
width: this.props.width,
}

// Every time we instantiate a new placeholder we record the index
// of its animation rules, then increment the global counter
ruleIndex: number = (() => {
const ruleIndex = globalRuleIndex;
globalRuleIndex += VENDOR_PREFIXES.length;
return ruleIndex;
})()

componentWillReceiveProps(newProps: Props) {
if (newProps.phase === this.props.phase &&
newProps.isDraggedOver === this.props.isDraggedOver) {
return;
}

if (newProps.phase === 'DRAGGING') {
const height = newProps.height || this.state.height;
const width = newProps.width || this.state.width;
const isAnimatingIn = newProps.isDraggedOver;
const isAnimatingOut = !newProps.isDraggedOver;
this.setState({ height, isAnimatingIn, isAnimatingOut, width });
}

// Reset the state once the drop completes
if (newProps.phase === 'DROP_COMPLETE') {
this.setState({
height: 0,
isAnimatingIn: false,
isAnimatingOut: false,
width: 0,
});
}
}

getAnimationKeyframes = (animationName: string): string[] => {
const { height, isAnimatingIn, isAnimatingOut, width } = this.state;
const expanded = `height: ${height}px; width: ${width}px;`;
const collapsed = 'height: 0; width: 0;';
const keyframeSteps = (() => {
if (isAnimatingIn) {
return `from { ${collapsed} } to { ${expanded} }`;
}
if (isAnimatingOut) {
return `from { ${expanded} } to { ${collapsed} }`;
}
return '';
})();

return VENDOR_PREFIXES.map(
prefix => `@${prefix}keyframes ${animationName} { ${keyframeSteps} }`
);
}

createAnimation = (): ?string => {
const { sheet } = stylesheet;

// Stop flow complaining about possibly undefined properties
if (!sheet ||
!sheet.cssRules ||
!sheet.insertRule ||
!sheet.deleteRule) {
return null;
}

// We need to generate a random name for the animation every time
// because you can't dynamically update CSS animations
const animationName = `rbdnd-placeholder-animation-${v4()}`;
const keyframes = this.getAnimationKeyframes(animationName);
// It's easier to manage if we inject a dummy rule when the
// browser doesn't support a prefixed version
const dummyRule = '.rbdnd-placeholder-donotuse { display: initial; }';
const { ruleIndex } = this;

for (let i = 0; i < VENDOR_PREFIXES.length; i++) {
const thisRuleIndex = ruleIndex + i;
// `sheet` is a CSSStyleSheet but the definition of HTMLStyleElement indicates that it
// contains a StyleSheet which doesn't have all the methods of its child CSSStyleSheet
// $ExpectError - property cssRules not found in StyleSheet
if (sheet.cssRules[thisRuleIndex]) {
// $ExpectError - property deleteRule not found in StyleSheet
sheet.deleteRule(thisRuleIndex);
}
// The browser will throw if it doesn't support a prefixed version of the rule
try {
// $ExpectError - property insertRule not found in StyleSheet
sheet.insertRule(keyframes[i], thisRuleIndex);
} catch (err) {
// If it doesn't like it we inject a dummy rule instead
// $ExpectError - property insertRule not found in StyleSheet
sheet.insertRule(dummyRule, thisRuleIndex);
}
}

return animationName;
}

getPlaceholderStyle = () => {
const { isDraggedOver, phase } = this.props;
const { height, width } = this.state;

const animationName = this.createAnimation();

const staticStyles = {
height,
pointerEvents: 'none',
width,
};

// Hold the full height during a drop
if (phase === 'DROP_ANIMATING' && isDraggedOver) {
return staticStyles;
}

// Animate in/out during a drag
if (phase === 'DRAGGING') {
return {
...staticStyles,
animationName,
animationDuration: transitionStyles.transitionTime,
animationTimingFunction: transitionStyles.transitionCurve,
animationDelay: '0.0s',
animationIterationCount: 1,
animationDirection: 'normal',
animationFillMode: 'forwards',
};
}

// Otherwise don't display
return {
display: 'none',
};
}

render() {
return (
<div style={this.getPlaceholderStyle()} />
);
}
}
21 changes: 21 additions & 0 deletions src/view/placeholder/StaticPlaceholder.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// @flow
import React, { PureComponent } from 'react';

type Props = {|
height: number,
width: number,
|};

export default class StaticPlaceholder extends PureComponent {
props: Props

render() {
const { height, width } = this.props;
const style = {
height,
pointerEvents: 'none',
width,
};
return <div style={style} />;
}
}
3 changes: 2 additions & 1 deletion src/view/placeholder/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export default from './placeholder';
export default from './Placeholder';
export { default as StaticPlaceholder } from './StaticPlaceholder';
20 changes: 0 additions & 20 deletions src/view/placeholder/placeholder.jsx

This file was deleted.