From 0299ae26a41eb571ea0cd5d8de9f487ceaa90343 Mon Sep 17 00:00:00 2001 From: Pete Hopkins Date: Tue, 22 Sep 2015 22:45:54 -0400 Subject: [PATCH] Crossfade example Adds an example to match the blog post, building a flipper, a slideDown, and a crossfade together. Also includes LoadingCrossfadeComponent. --- demo/components/emoji-span.jsx | 4 +- .../loading-crossfade-component.jsx | 81 +++++++++ demo/css/loading-placeholder.css | 42 +++++ demo/examples/crossfade-example.jsx | 171 ++++++++++++++++++ demo/examples/scrolling-group.jsx | 4 +- demo/main.jsx | 4 +- 6 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 demo/components/loading-crossfade-component.jsx create mode 100644 demo/css/loading-placeholder.css create mode 100644 demo/examples/crossfade-example.jsx diff --git a/demo/components/emoji-span.jsx b/demo/components/emoji-span.jsx index 88ae8ce..3ee076c 100644 --- a/demo/components/emoji-span.jsx +++ b/demo/components/emoji-span.jsx @@ -19,8 +19,10 @@ var EmojiSpan = React.createClass({ }, render: function () { + let {children, size, ...attrs} = this.props; + return ( - + ); }, }); diff --git a/demo/components/loading-crossfade-component.jsx b/demo/components/loading-crossfade-component.jsx new file mode 100644 index 0000000..bf9f6f5 --- /dev/null +++ b/demo/components/loading-crossfade-component.jsx @@ -0,0 +1,81 @@ +// Component for a VelocityTransitionGroup that crossfades between its children. + +// To use this component, render with a single child that contains the "loading" version of your +// UI. When data has loaded, switch the "key" of this child so that React considers it a brand +// new element and triggers the enter / leave effects. The two versions of the UI are expected to +// have identical heights. + +// Properties on this component are applied to the VelocityTransitionGroup component that this +// delegates to. A postion: 'relative' style is also applied since the loading effect requires +// position: 'absolute' on the child. + +// Use the property "opaque" if the children have opaque backgrounds. This will make the new element +// come in 100% opacity and fade the old element out from on top of it. (Without this, opaque +// elements end up bleeding the background behind the LoadingCrossfadeComponent through.) + +var React = require('react'); +var _ = require('lodash'); +var VelocityTransitionGroup = require('../../lib/velocity-transition-group'); + +var LoadingCrossfadeComponent = React.createClass({ + displayName: 'LoadingCrossfadeComponent', + + propTypes: { + opaque: React.PropTypes.bool, + duration: React.PropTypes.number, + }, + + getDefaultProps: function () { + return { + duration: 350, + }; + }, + + render: function () { + // position: 'relative' lets us absolutely-position the leaving child during the fade. + var style = _.defaults((this.props.style || {}), { position: 'relative' }); + + var transitionGroupProps = _.defaults(_.omit(this.props, 'children', 'style'), { + component: 'div', + style: style, + + enter: { + animation: { opacity: 1 }, + duration: this.props.duration, + style: { + // If we're animating opaque backgrounds then we just render the new element under the + // old one and fade out the old one. Without this, at e.g. the crossfade midpoint of + // 50% opacity for old and 50% opacity for new, the parent background ends up bleeding + // through 25%, which makes things look not smooth at all. + opacity: this.props.opaque ? 1 : 0, + + // We need to clear out all the styles that "leave" puts on the element. + position: 'relative', + top: '', + left: '', + bottom: '', + right: '', + zIndex: '', + }, + }, + + leave: { + animation: { opacity: 0 }, + duration: this.props.duration, + style: { + // 'absolute' so the 2 elements overlap for a crossfade + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + right: 0, + zIndex: 1, + }, + }, + }); + + return React.createElement(VelocityTransitionGroup, transitionGroupProps, this.props.children); + }, +}); + +module.exports = LoadingCrossfadeComponent; diff --git a/demo/css/loading-placeholder.css b/demo/css/loading-placeholder.css new file mode 100644 index 0000000..fd68297 --- /dev/null +++ b/demo/css/loading-placeholder.css @@ -0,0 +1,42 @@ +.loading-placeholder-light, +.loading-placeholder-dark { + position: relative; +} + +/* Gray, translucent bar to show "loading." Renders as 86% of the parent's height, vertically + centered, and 100% of its width. */ +.loading-placeholder-light:before, +.loading-placeholder-dark:before { + content: ' '; + display: block; + position: absolute; + top: 7%; + height: 86%; + width: 100%; +} + +.loading-placeholder-full:before { + top: 0; + height: 100%; +} + +/* Inserts a non-breaking space into the parent to guarantee that it has at least one line's + worth of height. (Necessary for the 'before' block to do a relatively-sized height.) */ +.loading-placeholder-light:after, +.loading-placeholder-dark:after { + content: '\00a0'; +} + +.loading-placeholder-light:before { + background-color: rgba(235, 237, 241, 0.1); +} + +.loading-placeholder-dark:before { + background-color: rgba(235, 237, 241, 0.5); +} + +span.loading-placeholder-light, +span.loading-placeholder-dark { + /* Useful for being able to set an explicit width on the placeholder. */ + display: inline-block; +} diff --git a/demo/examples/crossfade-example.jsx b/demo/examples/crossfade-example.jsx new file mode 100644 index 0000000..13c5cb2 --- /dev/null +++ b/demo/examples/crossfade-example.jsx @@ -0,0 +1,171 @@ +var _ = require('lodash'); + +var React = require('react'); +var VelocityComponent = require('../../lib/velocity-component'); +var VelocityTransitionGroup = require('../../lib/velocity-transition-group'); +var VelocityHelpers = require('../../lib/velocity-helpers'); + +var Box = require('../components/box'); +var EmojiSpan = require('../components/emoji-span'); +var LoadingCrossfadeComponent = require('../components/loading-crossfade-component'); + +require('../css/loading-placeholder.css'); + +var LOCATION_COUNT = 4; + +var BUILDINGS = ['🏠', '🏡', '🏢', '🏬', '🏭', '🏣', '🏤', '🏥', '🏦', '🏨', '🏩', '💒', '⛪', '🏪', '🏫']; +var CAPTIALS = ['Montgomery', 'Juneau', 'Phoenix', 'Little Rock', 'Sacramento', 'Denver', 'Hartford', 'Dover', + 'Tallahassee', 'Atlanta', 'Honolulu', 'Boise', 'Springfield', 'Indianapolis', 'Des Moines', 'Topeka', 'Frankfort', + 'Baton Rouge', 'Augusta', 'Annapolis', 'Boston', 'Lansing', 'St. Paul', 'Jackson', 'Jefferson City', 'Helena', + 'Lincoln', 'Carson City', 'Concord', 'Trenton', 'Santa Fe', 'Albany', 'Raleigh', 'Bismarck', 'Columbus', + 'Oklahoma City', 'Salem', 'Harrisburg', 'Providence', 'Columbia', 'Pierre', 'Nashville', 'Austin', 'Salt Lake City', + 'Montpelier', 'Richmond', 'Olympia', 'Charleston', 'Madison', 'Cheyenne']; + +var CrossfadeExample = React.createClass({ + displayName: 'CrossfadeExample', + + getInitialState: function () { + return { + expanded: false, + items: null, + duration: 500, + }; + }, + + componentWillUnmount: function () { + window.clearTimeout(this.locationTimeout); + }, + + whenToggleClicked: function () { + if (this.state.expanded) { + this.setState({ + expanded: false, + items: null, + }); + + window.clearTimeout(this.locationTimeout); + } else { + this.setState({ + expanded: true, + items: null, + }); + + this.locationTimeout = window.setTimeout(this.loadLocations, this.state.duration * 1.5); + } + }, + + loadLocations: function () { + this.setState({ + items: Array.apply(null, Array(LOCATION_COUNT)).map(function () { + return { + building: _.sample(BUILDINGS), + city: _.sample(CAPTIALS), + }; + }), + }); + }, + + whenOptionClicked: function (event) { + this.setState({ duration: parseInt(event.target.value) }); + }, + + render: function () { + var groupStyle = { + width: 198, + }; + + var boxStyle = { + margin: '-10px 0 0 0', + }; + + var toggleStyle = { + width: 200, + backgroundColor: '#3f83b7', + color: 'white', + padding: 8, + fontSize: 13, + lineHeight: '18px', + cursor: 'pointer', + }; + + var arrowStyle = { + display: 'block', + fontSize: 18, + }; + + return ( +
+
+ Points of Interest + + 👇 + +
+ + + {this.state.expanded ? this.renderLocations() : null} + + +
+ +   + +
+
+ ); + }, + + renderLocations: function () { + var boxStyle = { + backgroundColor: '#fefefe', + padding: '5px 10px', + }; + + var locations = this.state.items != null ? this.state.items : Array.apply(null, Array(LOCATION_COUNT)); + + return ( + +
{locations.map(this.renderLocation)}
+
+ ); + }, + + renderLocation: function(location, i) { + location = location || {building: '', city: ''}; + + var emojiStyle = { + fontSize: 30, + display: 'block', + width: 34.5, + height: 31, + }; + + var rowStyle = { + padding: '5px 0', + }; + + var cityStyle = { + fontSize: 16, + margin: '0 0 0 5px', + }; + + return ( +
+ {location.building} +
+ {location.city} +
+
+ ); + } +}); + +module.exports = CrossfadeExample; diff --git a/demo/examples/scrolling-group.jsx b/demo/examples/scrolling-group.jsx index 732e878..fdac24e 100644 --- a/demo/examples/scrolling-group.jsx +++ b/demo/examples/scrolling-group.jsx @@ -136,7 +136,7 @@ var ScrollingGroup = React.createClass({ {rows} -
+
@@ -144,7 +144,7 @@ var ScrollingGroup = React.createClass({ -
+ ); }, diff --git a/demo/main.jsx b/demo/main.jsx index d74fd6b..1b24bda 100644 --- a/demo/main.jsx +++ b/demo/main.jsx @@ -4,6 +4,7 @@ var React = require('react'); var VelocityComponent = require('../lib/velocity-component'); var VelocityTransitionGroup = require('../lib/velocity-transition-group'); +var CrossfadeExample = require('./examples/crossfade-example'); var FlapBox = require('./examples/flap-box'); var ScrollingGroup = require('./examples/scrolling-group'); var ToggleBox = require('./examples/toggle-box'); @@ -43,11 +44,12 @@ var Demo = React.createClass({ var MainComponent = React.createClass({ render: function () { return ( -
+
+
); },