From 42068dc3d53861e5286ca81302a0083c73ab4396 Mon Sep 17 00:00:00 2001 From: igorbt Date: Mon, 20 Jul 2015 00:35:33 +0300 Subject: [PATCH 1/2] implemented waterfall changed implementation for an optimized performance --- docs/src/app/app-routes.jsx | 101 ++--- .../src/app/components/AppBar/ExampleIcon.jsx | 1 + .../components/AppBar/ExampleIconButton.jsx | 1 + .../app/components/AppBar/ExampleIconMenu.jsx | 1 + .../examples/app-bar-waterfall-example.jsx | 79 ++++ .../components/examples/app-bar-waterfall.jsx | 74 ++++ docs/src/app/components/master.jsx | 357 ++++++++++++------ .../components/pages/components/app-bar.jsx | 6 + docs/src/app/components/pages/home.jsx | 97 +---- .../app/components/pages/page-with-nav.jsx | 3 +- src/app-bar.jsx | 203 +++++++++- src/app-canvas.jsx | 19 +- test/theming-v12-spec.js | 46 ++- 13 files changed, 669 insertions(+), 319 deletions(-) create mode 100644 docs/src/app/components/examples/app-bar-waterfall-example.jsx create mode 100644 docs/src/app/components/examples/app-bar-waterfall.jsx diff --git a/docs/src/app/app-routes.jsx b/docs/src/app/app-routes.jsx index 489ede43e53001..0ca61cff6d433d 100644 --- a/docs/src/app/app-routes.jsx +++ b/docs/src/app/app-routes.jsx @@ -49,6 +49,7 @@ import Tabs from './components/pages/components/tabs'; import TextFields from './components/pages/components/text-fields'; import TimePicker from './components/pages/components/time-picker'; import Toolbars from './components/pages/components/toolbars'; +import AppBarWaterfall from './components/examples/app-bar-waterfall'; /** @@ -61,57 +62,63 @@ import Toolbars from './components/pages/components/toolbars'; * handler and its parent handler like so: Paper > Components > Master */ const AppRoutes = ( - - - - - - - - + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + ); export default AppRoutes; diff --git a/docs/src/app/components/AppBar/ExampleIcon.jsx b/docs/src/app/components/AppBar/ExampleIcon.jsx index b4f6cd8bbcd0fd..959b96d7019b52 100644 --- a/docs/src/app/components/AppBar/ExampleIcon.jsx +++ b/docs/src/app/components/AppBar/ExampleIcon.jsx @@ -6,6 +6,7 @@ const AppBarExampleIcon = React.createClass({ return ( ); diff --git a/docs/src/app/components/AppBar/ExampleIconButton.jsx b/docs/src/app/components/AppBar/ExampleIconButton.jsx index 6fda172e7b6696..8a45f190cdda01 100644 --- a/docs/src/app/components/AppBar/ExampleIconButton.jsx +++ b/docs/src/app/components/AppBar/ExampleIconButton.jsx @@ -19,6 +19,7 @@ const AppBarExampleIconButton = React.createClass({ return ( Title} + position="static" iconElementLeft={} iconElementRight={} /> diff --git a/docs/src/app/components/AppBar/ExampleIconMenu.jsx b/docs/src/app/components/AppBar/ExampleIconMenu.jsx index 121d7c61bce2d5..f9471bfcac0986 100644 --- a/docs/src/app/components/AppBar/ExampleIconMenu.jsx +++ b/docs/src/app/components/AppBar/ExampleIconMenu.jsx @@ -11,6 +11,7 @@ const AppBarExampleIconMenu = React.createClass({ return ( } iconElementRight={ { this.titleEl = el; }}> + Waterfall AppBar + + } + iconElementLeft={ + + + + } + iconElementRight={ + + + + } + /> + ); + }, + + getWaterfallChildren() { + let styles = this.getStyles(); + return ( +
+ { this.logoEl = el; }} + style={styles.logo} + src="images/material-ui-logo.svg"/> +
+ ); + }, + + onHeightChange({height, minHeight, maxHeight}) { + let interpolation = (height - minHeight) / (maxHeight - minHeight); + + // For best performance, we will directly modify style on DOM elements + this.logoEl.style.transform = `translate3d(80px,0,0) scale3d(${interpolation}, ${interpolation}, 1)`; + this.logoEl.style.opacity = interpolation; + this.titleEl.style.opacity = 1 - interpolation; + }, + + onBackClick() { + window.history.back(); + }, + + getStyles() { + return { + logo: { + height: 120, + margin: '0 auto', + display: 'block', + transformOrigin: '25% 100% 0', + transform: 'translate3d(80px,0,0)', + }, + }; + }, +}); + +export default AppBarWaterfallExample; diff --git a/docs/src/app/components/examples/app-bar-waterfall.jsx b/docs/src/app/components/examples/app-bar-waterfall.jsx new file mode 100644 index 00000000000000..cc8667a0d6f1c0 --- /dev/null +++ b/docs/src/app/components/examples/app-bar-waterfall.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import {AppCanvas, Styles, Mixins} from 'material-ui'; + +import CodeExample from '../code-example/code-example'; +import FullWidthSection from '../full-width-section'; + +import AppBarWaterfallExample from './app-bar-waterfall-example'; +import AppBarWaterfallExampleCode from '!raw!./app-bar-waterfall-example'; + +const {StylePropable} = Mixins; +const {Typography} = Styles; +const ThemeManager = Styles.ThemeManager; +const DefaultRawTheme = Styles.LightRawTheme; + +const AppBarWaterfall = React.createClass({ + + mixins: [StylePropable], + + getInitialState() { + let muiTheme = ThemeManager.getMuiTheme(DefaultRawTheme); + // To switch to RTL... + // muiTheme.isRtl = true; + return { + muiTheme, + }; + }, + + contextTypes: { + router: React.PropTypes.func, + }, + + childContextTypes: { + muiTheme: React.PropTypes.object, + }, + + getChildContext() { + return { + muiTheme: this.state.muiTheme, + }; + }, + + getStyles() { + return { + headline: { + //headline + fontSize: '24px', + lineHeight: '32px', + paddingTop: '16px', + marginBottom: '12px', + letterSpacing: '0', + fontWeight: Typography.fontWeightNormal, + color: Typography.textDarkBlack, + }, + }; + }, + + render() { + let styles = this.getStyles(); + return ( + + + +

Waterfall AppBar

+ +

Here is an example of how you can obtain a nice animation effect on scroll + when using position waterfall.

+ +
+
+ ); + }, +}); + +export default AppBarWaterfall; diff --git a/docs/src/app/components/master.jsx b/docs/src/app/components/master.jsx index 478b4e88756589..77cfcfcbc8f6eb 100644 --- a/docs/src/app/components/master.jsx +++ b/docs/src/app/components/master.jsx @@ -1,24 +1,28 @@ import React from 'react'; +import ReactDom from 'react-dom'; import AppLeftNav from './app-left-nav'; import FullWidthSection from './full-width-section'; import {AppBar, AppCanvas, IconButton, EnhancedButton, + RaisedButton, Mixins, Styles, Tab, - Tabs, - Paper} from 'material-ui'; + Tabs} from 'material-ui'; -const {StylePropable} = Mixins; +const {StylePropable, StyleResizable} = Mixins; const {Colors, Spacing, Typography} = Styles; const ThemeManager = Styles.ThemeManager; const DefaultRawTheme = Styles.LightRawTheme; const Master = React.createClass({ - mixins: [StylePropable], + mixins: [ + StylePropable, + StyleResizable, + ], getInitialState() { let muiTheme = ThemeManager.getMuiTheme(DefaultRawTheme); @@ -61,16 +65,33 @@ const Master = React.createClass({ color: Colors.lightWhite, maxWidth: 335, }, - github: { - position: 'fixed', - right: Spacing.desktopGutter / 2, - top: 8, - zIndex: 5, - color: 'white', - }, - iconButton: { + githubButton2: { color: darkWhite, }, + container: { + position: 'absolute', + right: (Spacing.desktopGutter / 2) + 48, + bottom: 0, + }, + logoText: { + color: Colors.white, + fontWeight: Typography.fontWeightLight, + fontSize: 26, + }, + svgLogo: { + width: 65, + backgroundColor: Colors.cyan500, + marginRight: '-20px', + marginBottom: '-2px', + }, + tabs: { + width: 425, + bottom:0, + textTransform: 'uppercase', + }, + tab: { + height: 64, + }, }; }, @@ -79,7 +100,8 @@ const Master = React.createClass({ newMuiTheme.inkBar.backgroundColor = Colors.yellow200; this.setState({ muiTheme: newMuiTheme, - tabIndex: this._getSelectedIndex()}); + currentSection: this._getCurrentSectionFromHistory(), + }); let setTabsState = function() { this.setState({renderTabs: !(document.body.clientWidth <= 647)}); }.bind(this); @@ -90,35 +112,24 @@ const Master = React.createClass({ componentWillReceiveProps(nextProps, nextContext) { let newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme; this.setState({ - tabIndex: this._getSelectedIndex(), muiTheme: newMuiTheme, + currentSection: this._getCurrentSectionFromHistory(), }); }, render() { let styles = this.getStyles(); - let githubButton = ( - - ); - let githubButton2 = ( ); return ( - {githubButton} - {this.state.renderTabs ? this._getTabs() : this._getAppBar()} - + {this._getAppBar()} {this.props.children} @@ -134,127 +145,223 @@ const Master = React.createClass({ ); }, - _getTabs() { + _getSectionsData() { + return [ + { + route: '/get-started', + title: 'Get Started', + }, + { + route: '/customization', + title: 'Customization', + }, + { + route: '/components', + title: 'Components', + }, + ]; + }, + + _getAppBar() { + const styles = this.getStyles(); + + let + title = null, + tabs = null, + waterfall, + position = 'fixed' + ; + + if (this.state.renderTabs || !this.state.currentSection) { + title = ( + { this.logoElement = ReactDom.findDOMNode(el); }}> + + material ui + + ); + } else { + title = this.state.currentSection.title; + } + if (this.state.renderTabs) { + tabs = ( +
+ + {this._getSectionsData().map(section => + + )} + +
+ ); + } + + let githubButton = ( + + ); + + if (!this.state.currentSection) { + position = 'waterfall'; + waterfall = { + minHeight: 64, + maxHeight: 475 + 64, + // overflow hidden is needed because image may be translated outside + // of viewport creating horizontal scroll + children: this._getHomePageHero(), + }; + + waterfall.onHeightChange = ({height, minHeight, maxHeight}) => { + // interpolate opacity + let interpolation = (height - minHeight) / (maxHeight - minHeight); + + if (this.homePageHero) { + this.homePageHero.style.transform = 'scale3d(' + interpolation + ', ' + interpolation + ', 1)'; + this.homePageHero.style.transformOrigin = '50% 100% 0'; + this.homePageHero.style.opacity = interpolation; + } + + if (this.logoElement) { + this.logoElement.style.opacity = 1 - interpolation; + } + }; + } + + return ( + + {tabs} + + ); + }, + + + _getHomePageHero() { let styles = { root: { - backgroundColor: Colors.cyan500, - position: 'fixed', - height: 64, - top: 0, - right: 0, - zIndex: 1101, - width: '100%', + overflow: 'hidden', }, - container: { - position: 'absolute', - right: (Spacing.desktopGutter / 2) + 48, - bottom: 0, + svgLogo: { + marginLeft: (window.innerWidth * 0.5) - 130 + 'px', + width: 420, }, - span: { - color: Colors.white, + tagline: { + margin: '16px auto 0 auto', + textAlign: 'center', + maxWidth: 575, + }, + label: { + color: DefaultRawTheme.palette.primary1Color, + }, + githubStyle: { + margin: '16px 32px 0px 8px', + }, + demoStyle: { + margin: '16px 32px 0px 32px', + }, + h1: { + color: Colors.darkWhite, fontWeight: Typography.fontWeightLight, - left: 45, - top: 22, - position: 'absolute', - fontSize: 26, }, - svgLogoContainer: { - position: 'fixed', - width: 300, - left: Spacing.desktopGutter, + h2: { + fontSize: 20, + lineHeight: '28px', + paddingTop: 19, + marginBottom: 13, + letterSpacing: 0, }, - svgLogo: { - width: 65, - backgroundColor: Colors.cyan500, - position: 'absolute', - top: 20, + nowrap: { + whiteSpace: 'nowrap', }, - tabs: { - width: 425, - bottom:0, + taglineWhenLarge: { + marginTop: 32, }, - tab: { - height: 64, + h1WhenLarge: { + fontSize: 56, + }, + h2WhenLarge: { + fontSize: 24, + lineHeight: '32px', + paddingTop: 16, + marginBottom: 12, }, - }; - let materialIcon = this.state.tabIndex !== '0' ? ( - - - material ui - ) : null; + styles.h2 = this.mergeStyles(styles.h1, styles.h2); + + if (this.isDeviceSize(StyleResizable.statics.Sizes.LARGE)) { + styles.tagline = this.mergeStyles(styles.tagline, styles.taglineWhenLarge); + styles.h1 = this.mergeStyles(styles.h1, styles.h1WhenLarge); + styles.h2 = this.mergeStyles(styles.h2, styles.h2WhenLarge); + } return ( -
- - {materialIcon} -
- - - - - + +
{ this.homePageHero = el; }}> + +
+

material ui

+

+ A Set of React Components + that Implement + Google's Material Design +

+
- -
+
+ ); }, - _getSelectedIndex() { - return this.props.history.isActive('/get-started') ? '1' : - this.props.history.isActive('/customization') ? '2' : - this.props.history.isActive('/components') ? '3' : '0'; + _onDemoClick() { + this.props.history.pushState(null, '/components'); }, - _handleTabChange(value, e, tab) { - this.props.history.pushState(null, tab.props.route); - this.setState({tabIndex: this._getSelectedIndex()}); + _getCurrentSectionFromHistory() { + return this._getCurrentSection( + section => this.props.history.isActive(section.route) + ); }, - _getAppBar() { - let title = - this.props.history.isActive('/get-started') ? 'Get Started' : - this.props.history.isActive('/customization') ? 'Customization' : - this.props.history.isActive('/components') ? 'Components' : ''; - - let githubButton = ( - - ); + _getCurrentSection(test) { + const sections = this._getSectionsData(); + for (let i = 0; i < sections.length; i++) { + if (test(sections[i])) { + return sections[i]; + } + } + }, - return ( -
- -
); + _handleTabChange(value) { + // route is passed as value + this.props.history.pushState(null, value); + this.setState({currentSection: this._getCurrentSection(s => s.route === value)}); }, _onLeftIconButtonTouchTap() { diff --git a/docs/src/app/components/pages/components/app-bar.jsx b/docs/src/app/components/pages/components/app-bar.jsx index 4194fcce47a397..bb6a3b5b62668f 100644 --- a/docs/src/app/components/pages/components/app-bar.jsx +++ b/docs/src/app/components/pages/components/app-bar.jsx @@ -10,6 +10,7 @@ import AppBarExampleIconMenu from '../../AppBar/ExampleIconMenu'; import appBarExampleIconMenuCode from '!raw!../../AppBar/ExampleIconMenu'; import MarkdownElement from '../../MarkdownElement'; import appBarReadmeText from '../../AppBar/README'; +import RaisedButton from 'raised-button'; export default class AppBarPage extends React.Component { @@ -21,6 +22,11 @@ export default class AppBarPage extends React.Component { return (
+ diff --git a/docs/src/app/components/pages/home.jsx b/docs/src/app/components/pages/home.jsx index c2f12ed584f532..27d0e23fda3960 100644 --- a/docs/src/app/components/pages/home.jsx +++ b/docs/src/app/components/pages/home.jsx @@ -5,9 +5,7 @@ import HomeFeature from './home-feature'; import FullWidthSection from '../full-width-section'; const {StylePropable, StyleResizable} = Mixins; -const {Colors, Spacing, Typography} = Styles; -const DefaultRawTheme = Styles.LightRawTheme; - +const {Colors, Typography} = Styles; const HomePage = React.createClass({ @@ -18,13 +16,8 @@ const HomePage = React.createClass({ ], render() { - let style = { - paddingTop: Spacing.desktopKeylineIncrement, - }; - return ( -
- {this._getHomePageHero()} +
{this._getHomePurpose()} {this._getHomeFeatures()} {this._getHomeContribute()} @@ -32,88 +25,6 @@ const HomePage = React.createClass({ ); }, - _getHomePageHero() { - let styles = { - root: { - backgroundColor: Colors.cyan500, - overflow: 'hidden', - }, - svgLogo: { - marginLeft: (window.innerWidth * 0.5) - 130 + 'px', - width: 420, - }, - tagline: { - margin: '16px auto 0 auto', - textAlign: 'center', - maxWidth: 575, - }, - label: { - color: DefaultRawTheme.palette.primary1Color, - }, - githubStyle: { - margin: '16px 32px 0px 8px', - }, - demoStyle: { - margin: '16px 32px 0px 32px', - }, - h1: { - color: Colors.darkWhite, - fontWeight: Typography.fontWeightLight, - }, - h2: { - fontSize: 20, - lineHeight: '28px', - paddingTop: 19, - marginBottom: 13, - letterSpacing: 0, - }, - nowrap: { - whiteSpace: 'nowrap', - }, - taglineWhenLarge: { - marginTop: 32, - }, - h1WhenLarge: { - fontSize: 56, - }, - h2WhenLarge: { - fontSize: 24, - lineHeight: '32px', - paddingTop: 16, - marginBottom: 12, - }, - }; - - styles.h2 = this.mergeStyles(styles.h1, styles.h2); - - if (this.isDeviceSize(StyleResizable.statics.Sizes.LARGE)) { - styles.tagline = this.mergeStyles(styles.tagline, styles.taglineWhenLarge); - styles.h1 = this.mergeStyles(styles.h1, styles.h1WhenLarge); - styles.h2 = this.mergeStyles(styles.h2, styles.h2WhenLarge); - } - - return ( - - -
-

material ui

-

- A Set of React Components - that Implement - Google's Material Design -

- -
-
- ); - }, - _getHomePurpose() { let styles = { root: { @@ -205,10 +116,6 @@ const HomePage = React.createClass({ ); }, - - _onDemoClick() { - this.history.pushState(null, '/components'); - }, }); export default HomePage; diff --git a/docs/src/app/components/pages/page-with-nav.jsx b/docs/src/app/components/pages/page-with-nav.jsx index 9b03c36f8e832a..ca627e35438dc0 100644 --- a/docs/src/app/components/pages/page-with-nav.jsx +++ b/docs/src/app/components/pages/page-with-nav.jsx @@ -39,7 +39,6 @@ let PageWithNav = React.createClass({ let subNavWidth = Spacing.desktopKeylineIncrement * 3 + 'px'; let styles = { root: { - paddingTop: Spacing.desktopKeylineIncrement + 'px', }, rootWhenMedium: { position: 'relative', @@ -59,7 +58,7 @@ let PageWithNav = React.createClass({ secondaryNavWhenMedium: { borderTop: 'none', position: 'absolute', - top: '64px', + top: '0px', width: subNavWidth, }, contentWhenMedium: { diff --git a/src/app-bar.jsx b/src/app-bar.jsx index 046560617b5b2c..fc4913194b6c87 100644 --- a/src/app-bar.jsx +++ b/src/app-bar.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import ReactDOM from 'react-dom'; import StylePropable from './mixins/style-propable'; import Typography from './styles/typography'; import IconButton from './icon-button'; @@ -81,6 +82,14 @@ const AppBar = React.createClass({ */ onTitleTouchTap: React.PropTypes.func, + /** + * Specify position and behavior. Fixed - will have a fixed position at the top of viewport. + * Static - will have a static position. Waterfall - will have a fixed position at the top + * of viewport and will decrease its height on window scroll down (see waterfall prop for + * additional settings). + */ + position: React.PropTypes.oneOf(['fixed', 'static', 'waterfall']), + /** * Determines whether or not to display the Menu icon next to the title. * Setting this prop to false will hide the icon. @@ -95,13 +104,36 @@ const AppBar = React.createClass({ /** * The title to display on the app bar. */ - title: React.PropTypes.node, + title: React.PropTypes.oneOfType([ + React.PropTypes.node, + React.PropTypes.func, + ]), /** * Override the inline-styles of the app bar's title element. */ titleStyle: React.PropTypes.object, + /** + * Settings object for position waterfall. Should at least have minHeight + * and maxHeight properties, both numeric. These specify min and max visual heigth + * of the component while window scrolling. Optional children property can be a node + * or a function (will receive component styles object as argument) returning a node. This node will + * be inserted in the slide (scrolled) element of the component. Optional onHeightChange property + * is a function called when visual height of the component changes on scroll. This function will + * receive as arguments an object with height, minHeight, maxHeight and childrenEl (DOM element of + * the component) properties. Using onHeightChange, animation effects can be achieved + * by altering style properties of specific DOM elements. + */ + waterfall: React.PropTypes.shape({ + minHeight: React.PropTypes.number, + maxHeight: React.PropTypes.number, + onHeightChange: React.PropTypes.func, + children: React.PropTypes.oneOfType([ + React.PropTypes.node, + React.PropTypes.func, + ]), + }), /** * The zDepth of the app bar. * The shadow of the app bar is also dependent on this property. @@ -127,6 +159,7 @@ const AppBar = React.createClass({ showMenuIconButton: true, title: '', zDepth: 1, + position: 'fixed', }; }, @@ -146,6 +179,71 @@ const AppBar = React.createClass({ ); } } + + if (this.props.waterfall && this.props.waterfall.onHeightChange) { + this.setupWaterfall(); + } + }, + + componentDidUpdate: function(prevProps) { + if (this.props.waterfall && this.props.waterfall.onHeightChange) { + if (!(prevProps.waterfall && prevProps.waterfall.onHeightChange)) { + this.setupWaterfall(); + } + } else if (prevProps.waterfall && prevProps.waterfall.onHeightChange) { + this.removeWaterfall(); + } + }, + + componentWillUnmount: function() { + if (this.props.waterfall && this.props.waterfall.onHeightChange) { + this.removeWaterfall(); + } + }, + + setupWaterfall() { + // in some cases scroll event is not triggered + // after page reloaded and kept it's scroll + // so we call the handler from the start + this.waterfallScrollHandler(); + + ReactDOM.findDOMNode(this.refs.slideEl).style.position = 'absolute'; + + window.addEventListener('scroll', this.waterfallScrollHandler); + }, + + removeWaterfall() { + window.removeEventListener('scroll', this.waterfallScrollHandler); + }, + + waterfallScrollHandler() { + if (this.waterfallRunning) { return; } + this.waterfallRunning = true; + requestAnimationFrame(() => { + let waterfall = this.props.waterfall; + + let waterfallHeight = this.calculateWaterfallHeight(); + if (this.waterfallHeight !== waterfallHeight) { + this.waterfallHeight = waterfallHeight; + if (waterfall.onHeightChange) { + waterfall.onHeightChange({ + height: waterfallHeight, + maxHeight: waterfall.maxHeight, + minHeight: waterfall.minHeight, + childrenEl: ReactDOM.findDOMNode(this.refs.root), + }); + } + } + + this.waterfallRunning = false; + }); + + }, + + calculateWaterfallHeight() { + let waterfall = this.props.waterfall; + let windowScroll = window ? window.scrollY : 0; + return Math.max(waterfall.minHeight, waterfall.maxHeight - windowScroll); }, getStyles() { @@ -158,7 +256,8 @@ const AppBar = React.createClass({ let styles = { root: { - position: 'relative', + position: 'fixed', + top: 0, zIndex: rawTheme.zIndex.appBar, width: '100%', display: 'flex', @@ -217,6 +316,8 @@ const AppBar = React.createClass({ className, style, zDepth, + position, + waterfall, children, ...other, } = this.props; @@ -233,15 +334,22 @@ const AppBar = React.createClass({ if (title) { // If the title is a string, wrap in an h1 tag. // If not, just use it as a node. - titleElement = typeof title === 'string' || title instanceof String ? -

{title} -

: -
); + } else { + let titleNode = title; + if (typeof title === 'function') { + // pass styles, otherwise inaccesible + titleNode = title(this.getStyles()); + } + titleElement = (
- {title} -
; + {titleNode} +
); + } } if (showMenuIconButton) { @@ -304,19 +412,88 @@ const AppBar = React.createClass({ ); } - return ( - + + {/* this is the visual element that will slide. + position will be transformed to absolute in setupWaterfall + */} +
{waterfallChildren}
+ {/* this is the container for icons and children + * same styles ar for root but with no background - transparent */} +
+ {menuElementLeft} + {titleElement} + {menuElementRight} + {children} +
+
+ ); + } else { + let paperEl = ( {menuElementLeft} {titleElement} {menuElementRight} {children} - - ); + ); + + if (position === 'fixed') { + return ( +
{paperEl}
+ ); + } else if (position === 'static') { + return paperEl; + } + } }, _onLeftIconButtonTouchTap(event) { diff --git a/src/app-canvas.jsx b/src/app-canvas.jsx index 370ce35142566e..270c454d342038 100644 --- a/src/app-canvas.jsx +++ b/src/app-canvas.jsx @@ -47,26 +47,9 @@ const AppCanvas = React.createClass({ direction: 'ltr', }; - let newChildren = React.Children.map(this.props.children, (currentChild) => { - if (!currentChild) { // If undefined, skip it - return null; - } - - switch (currentChild.type.displayName) { - case 'AppBar' : - return React.cloneElement(currentChild, { - style: this.mergeStyles(currentChild.props.style, { - position: 'fixed', - }), - }); - default: - return currentChild; - } - }, this); - return (
- {newChildren} + {this.props.children}
); }, diff --git a/test/theming-v12-spec.js b/test/theming-v12-spec.js index 771cf5f8e04283..4f430d307e85fa 100644 --- a/test/theming-v12-spec.js +++ b/test/theming-v12-spec.js @@ -4,12 +4,14 @@ const AppBar = require('app-bar'); const RaisedButton = require('raised-button'); const React = require('react'); +const ReactDOM = require('react-dom'); const TestUtils = require('react-addons-test-utils'); const ThemeManager = require('styles/theme-manager'); const ThemeDecorator = require('styles/theme-decorator'); const DarkRawTheme = require('styles/raw-themes/dark-raw-theme'); const LightRawTheme = require('styles/raw-themes/light-raw-theme'); const Colors = require('styles/colors'); +const Paper = require('paper'); describe('Theming', () => { describe('ThemeManager', () => { @@ -38,10 +40,11 @@ describe('Theming', () => { describe('When no theme is specified, AppBar', () => { it('should display with default light theme', () => { let renderedAppbar = TestUtils.renderIntoDocument(); - let appbarDivs = TestUtils.scryRenderedDOMComponentsWithTag(renderedAppbar, 'div'); - let firstDiv = appbarDivs[0]; + let paperEl = ReactDOM.findDOMNode( + TestUtils.scryRenderedComponentsWithType(renderedAppbar, Paper)[0] + ); - expect(firstDiv.style.backgroundColor).to.equal('rgb(0, 188, 212)'); + expect(paperEl.style.backgroundColor).to.equal('rgb(0, 188, 212)'); }); }); @@ -52,22 +55,24 @@ describe('Theming', () => { it('should display with passed down dark theme', () => { let renderedAppbar = TestUtils.renderIntoDocument(); - let appbarDivs = TestUtils.scryRenderedDOMComponentsWithTag(renderedAppbar, 'div'); - let firstDiv = appbarDivs[0]; + let paperEl = ReactDOM.findDOMNode( + TestUtils.scryRenderedComponentsWithType(renderedAppbar, Paper)[0] + ); - expect(firstDiv.style.backgroundColor).to.equal('rgb(0, 151, 167)'); + expect(paperEl.style.backgroundColor).to.equal('rgb(0, 151, 167)'); }); it('should display with passed down dark theme and overriden specific attribute', () => { let renderedAppbar = TestUtils.renderIntoDocument(); - let appbarDivs = TestUtils.scryRenderedDOMComponentsWithTag(renderedAppbar, 'div'); - let firstDiv = appbarDivs[0]; + let paperEl = ReactDOM.findDOMNode( + TestUtils.scryRenderedComponentsWithType(renderedAppbar, Paper)[0] + ); let appbarH1s = TestUtils.scryRenderedDOMComponentsWithTag(renderedAppbar, 'h1'); let firstH1 = appbarH1s[0]; - expect(firstDiv.style.backgroundColor).to.equal('rgb(0, 151, 167)'); + expect(paperEl.style.backgroundColor).to.equal('rgb(0, 151, 167)'); expect(firstH1.style.color).to.equal('rgb(98, 0, 234)'); }); @@ -77,21 +82,23 @@ describe('Theming', () => { it('should display with passed down dark theme', () => { let renderedAppbar = TestUtils.renderIntoDocument(); - let appbarDivs = TestUtils.scryRenderedDOMComponentsWithTag(renderedAppbar, 'div'); - let firstDiv = appbarDivs[0]; + let paperEl = ReactDOM.findDOMNode( + TestUtils.scryRenderedComponentsWithType(renderedAppbar, Paper)[0] + ); - expect(firstDiv.style.backgroundColor).to.equal('rgb(0, 151, 167)'); + expect(paperEl.style.backgroundColor).to.equal('rgb(0, 151, 167)'); }); it('should display with passed down dark theme and overriden specific attribute', () => { let renderedAppbar = TestUtils.renderIntoDocument(); - let appbarDivs = TestUtils.scryRenderedDOMComponentsWithTag(renderedAppbar, 'div'); - let firstDiv = appbarDivs[0]; + let paperEl = ReactDOM.findDOMNode( + TestUtils.scryRenderedComponentsWithType(renderedAppbar, Paper)[0] + ); let appbarH1s = TestUtils.scryRenderedDOMComponentsWithTag(renderedAppbar, 'h1'); let firstH1 = appbarH1s[0]; - expect(firstDiv.style.backgroundColor).to.equal('rgb(0, 151, 167)'); + expect(paperEl.style.backgroundColor).to.equal('rgb(0, 151, 167)'); expect(firstH1.style.color).to.equal('rgb(98, 0, 234)'); }); @@ -102,21 +109,22 @@ describe('Theming', () => { it('should display with updated theme', () => { let renderedComponent = TestUtils.renderIntoDocument(); - let componentDivs = TestUtils.scryRenderedDOMComponentsWithTag(renderedComponent, 'div'); - let appbarDiv = componentDivs[1]; + let paperEl = ReactDOM.findDOMNode( + TestUtils.scryRenderedComponentsWithType(renderedComponent, Paper)[0] + ); let buttonNode = (TestUtils.scryRenderedDOMComponentsWithTag(renderedComponent, 'button'))[1]; let appbarH1s = TestUtils.scryRenderedDOMComponentsWithTag(renderedComponent, 'h1'); let firstH1 = appbarH1s[0]; - expect(appbarDiv.style.backgroundColor).to.equal('rgb(0, 151, 167)'); + expect(paperEl.style.backgroundColor).to.equal('rgb(0, 151, 167)'); expect(firstH1.style.color).to.equal('rgb(48, 48, 48)'); //simulate button click TestUtils.Simulate.click(buttonNode); //now new theme should be applied and text color of app bar should be changed - expect(appbarDiv.style.backgroundColor).to.equal('rgb(0, 188, 212)'); + expect(paperEl.style.backgroundColor).to.equal('rgb(0, 188, 212)'); expect(firstH1.style.color).to.equal('rgb(98, 0, 234)'); }); }); From 259f7b821d59bf2c4febdd18aa873401063a097b Mon Sep 17 00:00:00 2001 From: igorbt Date: Sat, 12 Dec 2015 00:47:14 +0200 Subject: [PATCH 2/2] code review corrections --- docs/src/app/app-routes.jsx | 101 ++++++++---------- .../components/AppBar/ExampleWaterfall.jsx | 94 ++++++++++++++++ .../ExampleWaterfallOptimized.jsx} | 54 ++++++---- .../components/examples/app-bar-waterfall.jsx | 74 ------------- .../components/pages/components/app-bar.jsx | 73 +++++++++++-- src/app-bar.jsx | 11 +- 6 files changed, 247 insertions(+), 160 deletions(-) create mode 100644 docs/src/app/components/AppBar/ExampleWaterfall.jsx rename docs/src/app/components/{examples/app-bar-waterfall-example.jsx => AppBar/ExampleWaterfallOptimized.jsx} (61%) delete mode 100644 docs/src/app/components/examples/app-bar-waterfall.jsx diff --git a/docs/src/app/app-routes.jsx b/docs/src/app/app-routes.jsx index 0ca61cff6d433d..489ede43e53001 100644 --- a/docs/src/app/app-routes.jsx +++ b/docs/src/app/app-routes.jsx @@ -49,7 +49,6 @@ import Tabs from './components/pages/components/tabs'; import TextFields from './components/pages/components/text-fields'; import TimePicker from './components/pages/components/time-picker'; import Toolbars from './components/pages/components/toolbars'; -import AppBarWaterfall from './components/examples/app-bar-waterfall'; /** @@ -62,63 +61,57 @@ import AppBarWaterfall from './components/examples/app-bar-waterfall'; * handler and its parent handler like so: Paper > Components > Master */ const AppRoutes = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + ); export default AppRoutes; diff --git a/docs/src/app/components/AppBar/ExampleWaterfall.jsx b/docs/src/app/components/AppBar/ExampleWaterfall.jsx new file mode 100644 index 00000000000000..d265cb29bc12a9 --- /dev/null +++ b/docs/src/app/components/AppBar/ExampleWaterfall.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import {AppBar} from 'material-ui'; + +import IconButton from 'icon-button'; +import MoreVertIcon from 'svg-icons/navigation/more-vert'; +import ArrowBack from 'svg-icons/navigation/arrow-back'; + +const MIN_HEIGHT = 64; +const MAX_HEIGHT = 210; + +const AppBarWaterfallExample = React.createClass({ + + propTypes: { + onBack: React.PropTypes.func, + }, + + getInitialState() { + return { + height: MAX_HEIGHT, + }; + }, + + render() { + const styles = this.getStyles(); + return ( + + { this.logoEl = el; }} + style={styles.logo} + src="images/material-ui-logo.svg"/> +
), + }} + title={ +
{ this.titleEl = el; }}> + Waterfall AppBar +
+ } + iconElementLeft={ + + + + } + iconElementRight={ + + + + } + /> + ); + }, + + onHeightChange({height}) { + this.setState({height}); + }, + + onBackClick() { + this.props.onBack(); + }, + + getInterpolation(height) { + return (height - MIN_HEIGHT) / (MAX_HEIGHT - MIN_HEIGHT); + }, + + getStyles() { + const interpolation = this.getInterpolation(this.state.height); + return { + logoWrap: { + overflow: 'hidden', + }, + logo: { + height: 120, + margin: '0 auto', + display: 'block', + transformOrigin: '25% 100% 0', + transform: `translate3d(80px,0,0) scale3d(${interpolation}, ${interpolation}, 1)`, + opacity: interpolation, + }, + title: { + opacity: 1 - interpolation, + }, + }; + }, +}); + +export default AppBarWaterfallExample; diff --git a/docs/src/app/components/examples/app-bar-waterfall-example.jsx b/docs/src/app/components/AppBar/ExampleWaterfallOptimized.jsx similarity index 61% rename from docs/src/app/components/examples/app-bar-waterfall-example.jsx rename to docs/src/app/components/AppBar/ExampleWaterfallOptimized.jsx index 68ae9f3377a224..92b44f03ca2aac 100644 --- a/docs/src/app/components/examples/app-bar-waterfall-example.jsx +++ b/docs/src/app/components/AppBar/ExampleWaterfallOptimized.jsx @@ -5,20 +5,35 @@ import IconButton from 'icon-button'; import MoreVertIcon from 'svg-icons/navigation/more-vert'; import ArrowBack from 'svg-icons/navigation/arrow-back'; +const MIN_HEIGHT = 64; +const MAX_HEIGHT = 210; + const AppBarWaterfallExample = React.createClass({ + propTypes: { + onBack: React.PropTypes.func, + }, + render() { + const styles = this.getStyles(); return ( + { this.logoEl = el; }} + style={styles.logo} + src="images/material-ui-logo.svg"/> +
), }} title={
{ this.titleEl = el; }}> Waterfall AppBar
@@ -37,40 +52,39 @@ const AppBarWaterfallExample = React.createClass({ ); }, - getWaterfallChildren() { - let styles = this.getStyles(); - return ( -
- { this.logoEl = el; }} - style={styles.logo} - src="images/material-ui-logo.svg"/> -
- ); - }, - - onHeightChange({height, minHeight, maxHeight}) { - let interpolation = (height - minHeight) / (maxHeight - minHeight); + onHeightChange({height}) { + let interpolation = this.getInterpolation(height); // For best performance, we will directly modify style on DOM elements this.logoEl.style.transform = `translate3d(80px,0,0) scale3d(${interpolation}, ${interpolation}, 1)`; + this.logoEl.style.opacity = interpolation; this.titleEl.style.opacity = 1 - interpolation; }, onBackClick() { - window.history.back(); + this.props.onBack(); + }, + + getInterpolation(height) { + return (height - MIN_HEIGHT) / (MAX_HEIGHT - MIN_HEIGHT); }, getStyles() { return { + logoWrap: { + overflow: 'hidden', + }, logo: { height: 120, margin: '0 auto', display: 'block', transformOrigin: '25% 100% 0', - transform: 'translate3d(80px,0,0)', + transform: `translate3d(80px,0,0) scale3d(1, 1, 1)`, + opacity: 1, + }, + title: { + opacity: 0, }, }; }, diff --git a/docs/src/app/components/examples/app-bar-waterfall.jsx b/docs/src/app/components/examples/app-bar-waterfall.jsx deleted file mode 100644 index cc8667a0d6f1c0..00000000000000 --- a/docs/src/app/components/examples/app-bar-waterfall.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import {AppCanvas, Styles, Mixins} from 'material-ui'; - -import CodeExample from '../code-example/code-example'; -import FullWidthSection from '../full-width-section'; - -import AppBarWaterfallExample from './app-bar-waterfall-example'; -import AppBarWaterfallExampleCode from '!raw!./app-bar-waterfall-example'; - -const {StylePropable} = Mixins; -const {Typography} = Styles; -const ThemeManager = Styles.ThemeManager; -const DefaultRawTheme = Styles.LightRawTheme; - -const AppBarWaterfall = React.createClass({ - - mixins: [StylePropable], - - getInitialState() { - let muiTheme = ThemeManager.getMuiTheme(DefaultRawTheme); - // To switch to RTL... - // muiTheme.isRtl = true; - return { - muiTheme, - }; - }, - - contextTypes: { - router: React.PropTypes.func, - }, - - childContextTypes: { - muiTheme: React.PropTypes.object, - }, - - getChildContext() { - return { - muiTheme: this.state.muiTheme, - }; - }, - - getStyles() { - return { - headline: { - //headline - fontSize: '24px', - lineHeight: '32px', - paddingTop: '16px', - marginBottom: '12px', - letterSpacing: '0', - fontWeight: Typography.fontWeightNormal, - color: Typography.textDarkBlack, - }, - }; - }, - - render() { - let styles = this.getStyles(); - return ( - - - -

Waterfall AppBar

- -

Here is an example of how you can obtain a nice animation effect on scroll - when using position waterfall.

- -
-
- ); - }, -}); - -export default AppBarWaterfall; diff --git a/docs/src/app/components/pages/components/app-bar.jsx b/docs/src/app/components/pages/components/app-bar.jsx index bb6a3b5b62668f..ab18a577ca011a 100644 --- a/docs/src/app/components/pages/components/app-bar.jsx +++ b/docs/src/app/components/pages/components/app-bar.jsx @@ -8,25 +8,41 @@ import AppBarExampleIconButton from '../../AppBar/ExampleIconButton'; import appBarExampleIconButtonCode from '!raw!../../AppBar/ExampleIconButton'; import AppBarExampleIconMenu from '../../AppBar/ExampleIconMenu'; import appBarExampleIconMenuCode from '!raw!../../AppBar/ExampleIconMenu'; +import AppBarExampleWaterfallOptimized from '../../AppBar/ExampleWaterfallOptimized'; +import appBarExampleWaterfallOptimizedCode from '!raw!../../AppBar/ExampleWaterfallOptimized'; +import AppBarExampleWaterfall from '../../AppBar/ExampleWaterfall'; +import appBarExampleWaterfallCode from '!raw!../../AppBar/ExampleWaterfall'; import MarkdownElement from '../../MarkdownElement'; import appBarReadmeText from '../../AppBar/README'; -import RaisedButton from 'raised-button'; +import Toggle from 'toggle'; export default class AppBarPage extends React.Component { - constructor(props) { super(props); + this.state = { + waterfallVisible: false, + waterfallOptimized: true, + }; } render() { + const styles = { + toggle: { + maxWidth: 250, + }, + }; return (
+ { + this.state.waterfallVisible ? + this.state.waterfallOptimized ? + + : + + : + null + } - @@ -36,8 +52,51 @@ export default class AppBarPage extends React.Component { + + + +
); } + + onWaterfallBack = () => { + this.setState({waterfallVisible: false}); + } + + onWaterfallToggle = (event, toggled) => { + this.setState({waterfallVisible: toggled}); + + // animated scroll + function scrollTo(element, to, duration) { + if (duration <= 0) return; + let difference = to - element.scrollTop; + let perTick = difference / duration * 10; + + setTimeout(function() { + element.scrollTop = element.scrollTop + perTick; + if (element.scrollTop === to) return; + scrollTo(element, to, duration - 10); + }, 10); + } + + if (toggled) { + scrollTo(document.body, 0, 1000); + } + } + + onWaterfallOptimizeToggle = (event, toggled) => { + this.setState({waterfallOptimized: toggled}); + } } diff --git a/src/app-bar.jsx b/src/app-bar.jsx index fc4913194b6c87..2d1674d5cdca04 100644 --- a/src/app-bar.jsx +++ b/src/app-bar.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import StylePropable from './mixins/style-propable'; import Typography from './styles/typography'; import IconButton from './icon-button'; @@ -207,7 +206,7 @@ const AppBar = React.createClass({ // so we call the handler from the start this.waterfallScrollHandler(); - ReactDOM.findDOMNode(this.refs.slideEl).style.position = 'absolute'; + this.waterfallSlideEL.style.position = 'absolute'; window.addEventListener('scroll', this.waterfallScrollHandler); }, @@ -230,7 +229,7 @@ const AppBar = React.createClass({ height: waterfallHeight, maxHeight: waterfall.maxHeight, minHeight: waterfall.minHeight, - childrenEl: ReactDOM.findDOMNode(this.refs.root), + childrenEl: this.waterfallRootEl, }); } } @@ -258,6 +257,7 @@ const AppBar = React.createClass({ root: { position: 'fixed', top: 0, + left: 0, zIndex: rawTheme.zIndex.appBar, width: '100%', display: 'flex', @@ -429,7 +429,7 @@ const AppBar = React.createClass({
{ this.waterfallRootEl = el; }} style={{ height: waterfall.maxHeight, }}> @@ -443,10 +443,11 @@ const AppBar = React.createClass({ position will be transformed to absolute in setupWaterfall */}
{ this.waterfallSlideEL = el; }} style={{ position: 'fixed', top: 0, + left: 0, zIndex: paperElStyle.zIndex + 1, width: '100%', height: waterfall.maxHeight,