Repository: FormidableLabs/spectacle
Pull request #349 (2017-08-08 01:41:44)

Update README to explain how to override progress bar colour
https://github.com/FormidableLabs/spectacle/issues/328


Patch:
diff --git a/README.md b/README.md
index 947d3ef..6cd47f8 100644
--- a/README.md
+++ b/README.md
@@ -68,12 +68,12 @@ But really, it is SO much easier to just use the boilerplate. Trust me.
 <a name="one-page"></a>
 ## One Page
 
-To aid with speedy development / kicking the tires on spectacle, we suuport using a simple boilerplate HTML page with a bespoke script tag that contains your entire presentation. The rest of the setup will take care of transpiling your React/ESnext code, providing Spectacle, React, and ReactDOM libraries, and being raring to go with a minimum of effort.
+To aid with speedy development / kicking the tires on spectacle, we support using a simple boilerplate HTML page with a bespoke script tag that contains your entire presentation. The rest of the setup will take care of transpiling your React/ESnext code, providing Spectacle, React, and ReactDOM libraries, and being raring to go with a minimum of effort.
 
-We can start with this projet's sample at [`one-page.html`](./one-page.html). It's essentially, the same presentation as the fully-built-from-source version, with a few notable exceptions:
+We can start with this project's sample at [`one-page.html`](./one-page.html). It's essentially, the same presentation as the fully-built-from-source version, with a few notable exceptions:
 
 1. There are no `import`s or `require`s. Everything must come from the global namespace. This includes `Spectacle`, `React`, `ReactDOM` and all the Spectacle exports from [`./src/index.js`](./src/index.js) -- `Deck`, `Slide`, `themes`, etc.
-2. The presentation must include exaclty **one** script tag with the type `text/spectacle` that is a function. Presently, that function is directly inserted inline into a wrapper code boilerplate as a React Component `render` function. The wrapper is transpiled. There should not be any extraneous content around it like outer variables or comments.
+2. The presentation must include exactly **one** script tag with the type `text/spectacle` that is a function. Presently, that function is directly inserted inline into a wrapper code boilerplate as a React Component `render` function. The wrapper is transpiled. There should not be any extraneous content around it like outer variables or comments.
 
     **Good** examples:
 
@@ -210,6 +210,7 @@ You can toggle the presenter or overview mode by pressing respectively `alt+p` a
 |Alt/Option + O|Toggle Overview Mode|
 |Alt/Option + P|Toggle Presenter Mode|
 |Alt/Option + T|Toggle Timer in Presenter Mode|
+|Alt/Option + A|Start autoplay (if enabled)|
 
 <a name="fullscreen"></a>
 ## Fullscreen
@@ -322,10 +323,12 @@ The Deck tag is the root level tag for your presentation. It supports the follow
 |---|---|---|
 |controls| PropTypes.bool| Show control arrows when not in fullscreen
 |history|PropTypes.object|Accepts custom configuration for [history](https://github.com/ReactTraining/history)
-|progress| PropTypes.string|Accepts `pacman`, `bar`, `number` or `none`.
+|progress| PropTypes.string|Accepts `pacman`, `bar`, `number` or `none`. To override the color, change the 'quarternary' color in the theme.
 |theme|PropTypes.object|Accepts a theme object for styling your presentation|
 |transition|PropTypes.array|Accepts `slide`, `zoom`, `fade` or `spin`, and can be combined. Sets global slide transitions. **Note: If you use the 'scale' transition, fitted text won't work in Safari.**|
 |transitionDuration| PropTypes.number| Accepts integer value in milliseconds for global transition duration.
+|autoplay|PropTypes.bool| Automatically advance slides.
+|autoplayDuration|PropTypes.number| Accepts integer value in milliseconds for global autoplay duration, defaults to 7000.
 
 <a name="slide-base"></a>
 #### Slide (Base)
@@ -337,7 +340,7 @@ The slide tag represents each slide in the presentation. Giving a slide tag an `
 |align| PropTypes.string | Accepts a space delimited value for positioning interior content. The first value can be `flex-start` (left), `center` (middle), or `flex-end` (right). The second value can be `flex-start` (top) , `center` (middle), or `flex-end` (bottom). You would provide this prop like `align="center center"`, which is its default.
 |id| PropTypes.string | Used to create a string based hash.
 |maxHeight| PropTypes.number | Used to set max dimensions of the Slide.
-|maxWidth| PropTypes.number | Used to set max dimentions of the Slide.
+|maxWidth| PropTypes.number | Used to set max dimensions of the Slide.
 |notes| PropTypes.string| Text which will appear in the presenter mode. Can be HTML.
 |transition|PropTypes.array|Accepts `slide`, `zoom`, `fade` or `spin`, and can be combined. Sets the slide transition. **Note: If you use the 'scale' transition, fitted text won't work in Safari.**|
 |transitionDuration| PropTypes.number| Accepts integer value in milliseconds for slide transition duration.
diff --git a/example/src/index.js b/example/src/index.js
index fc5bf1a..8ab2b8a 100644
--- a/example/src/index.js
+++ b/example/src/index.js
@@ -31,7 +31,7 @@ const theme = createTheme({
 export default class Presentation extends React.Component {
   render() {
     return (
-      <Deck transition={["zoom", "slide"]} theme={theme} transitionDuration={500}>
+      <Deck autoplay transition={["zoom", "slide"]} theme={theme} transitionDuration={500}>
         <Slide transition={["zoom"]} bgColor="primary">
           <Heading size={1} fit caps lineHeight={1} textColor="black">
             Spectacle
@@ -82,6 +82,26 @@ export default class Presentation extends React.Component {
             </Heading>
           </Appear>
         </Slide>
+        <Slide transition={["fade"]}>
+          <Layout style={{position: 'relative', width: '100%'}}>
+            <Fill width="100%">
+              <Image src={images.city.replace("/", "")} style={{width: '100%'}} />
+            </Fill>
+            <Appear fid="1" transitionDuration={500}>
+              <Fill style={{position: 'absolute', maxHeight: 700, maxWidth: 1000, width: '100%', height: '100%'}}>
+                <Heading size={3} caps fit textColor="primary">
+                  Overlay Text
+                </Heading>
+                <Heading size={3} caps fit textColor="primary">
+                  <Image src={images.logo} />
+                </Heading>
+                <Heading size={3} caps fit textColor="primary">
+                  and Images
+                </Heading>
+              </Fill>
+            </Appear>
+          </Layout>
+        </Slide>
         <Slide transition={["zoom", "fade"]} bgColor="primary">
           <Heading caps fit>Flexible Layouts</Heading>
           <Layout>
diff --git a/package.json b/package.json
index 80be67b..f3868ae 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "spectacle",
-  "version": "2.1.2",
+  "version": "2.2.0",
   "description": "ReactJS Powered Presentation Framework",
   "main": "lib/index.js",
   "scripts": {
diff --git a/src/components/appear.js b/src/components/appear.js
index ecbf83c..79cc9bf 100644
--- a/src/components/appear.js
+++ b/src/components/appear.js
@@ -50,7 +50,7 @@ class Appear extends Component {
         {({ opacity }) =>
           React.cloneElement(child, {
             className: 'fragment',
-            style: { opacity },
+            style: { ...child.props.style, opacity },
             ref: f => {
               this.fragmentRef = f;
             },
diff --git a/src/components/autoplay-controls.js b/src/components/autoplay-controls.js
new file mode 100644
index 0000000..0b87f8c
--- /dev/null
+++ b/src/components/autoplay-controls.js
@@ -0,0 +1,67 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Radium from 'radium';
+
+@Radium
+export default class AutoplayControls extends Component {
+  render() {
+    const styles = {
+      opacity: 0,
+      cursor: 'pointer',
+      transition: '300ms opacity ease',
+      ':hover': {
+        opacity: 1
+      }
+    };
+
+    const pauseBtn = (
+      <button
+        type="button"
+        key="pause"
+        onClick={this.props.onPause}
+        style={[styles, this.context.styles.autoplay.pause]}
+      >
+        <svg
+          style={this.context.styles.autoplay.pauseIcon}
+          xmlns="http://www.w3.org/2000/svg"
+          width="30px"
+          height="30px"
+          viewBox="0 0 30 30"
+        >
+          <path d="M23.5,4V26h-6V4ZM6.5,26h6V4h-6Z"/>
+        </svg>
+      </button>
+    );
+
+    const playBtn = (
+      <button
+        type="button"
+        key="play"
+        onClick={this.props.onPlay}
+        style={[styles, this.context.styles.autoplay.play]}
+      >
+        <svg
+          style={this.context.styles.autoplay.playIcon}
+          xmlns="http://www.w3.org/2000/svg"
+          width="30px"
+          height="30px"
+          viewBox="0 0 30 30"
+        >
+          <path d="M26,15,6,25V5Z"/>
+        </svg>
+      </button>
+    );
+
+    return this.props.autoplaying ? pauseBtn : playBtn;
+  }
+}
+
+AutoplayControls.propTypes = {
+  autoplaying: PropTypes.bool,
+  onPause: PropTypes.func,
+  onPlay: PropTypes.func,
+};
+
+AutoplayControls.contextTypes = {
+  styles: PropTypes.object
+};
diff --git a/src/components/deck.js b/src/components/deck.js
index 1d8be6d..47f8e59 100644
--- a/src/components/deck.js
+++ b/src/components/deck.js
@@ -13,6 +13,8 @@ export default class Deck extends Component {
   static displayName = 'Deck';
 
   static propTypes = {
+    autoplay: PropTypes.bool,
+    autoplayDuration: PropTypes.number,
     children: PropTypes.node,
     controls: PropTypes.bool,
     globalStyles: PropTypes.bool,
@@ -20,7 +22,7 @@ export default class Deck extends Component {
     progress: PropTypes.oneOf(['pacman', 'bar', 'number', 'none']),
     theme: PropTypes.object,
     transition: PropTypes.array,
-    transitionDuration: PropTypes.number
+    transitionDuration: PropTypes.number,
   };
 
   render() {
diff --git a/src/components/manager.js b/src/components/manager.js
index 0935b6f..2d0326f 100644
--- a/src/components/manager.js
+++ b/src/components/manager.js
@@ -17,6 +17,7 @@ import Presenter from './presenter';
 import Export from './export';
 import Overview from './overview';
 
+import AutoplayControls from './autoplay-controls';
 import Fullscreen from './fullscreen';
 import Progress from './progress';
 import Controls from './controls';
@@ -28,6 +29,8 @@ export default class Manager extends Component {
   static displayName = 'Manager';
 
   static defaultProps = {
+    autoplay: false,
+    autoplayDuration: 7000,
     transition: [],
     transitionDuration: 500,
     progress: 'pacman',
@@ -36,6 +39,8 @@ export default class Manager extends Component {
   };
 
   static propTypes = {
+    autoplay: PropTypes.bool,
+    autoplayDuration: PropTypes.number,
     children: PropTypes.node,
     controls: PropTypes.bool,
     dispatch: PropTypes.func,
@@ -58,17 +63,20 @@ export default class Manager extends Component {
     slide: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
   };
 
-  constructor() {
-    super();
+  constructor(props) {
+    super(props);
     this._handleKeyPress = this._handleKeyPress.bind(this);
     this._handleScreenChange = this._handleScreenChange.bind(this);
     this.handleClick = this.handleClick.bind(this);
     this._goToSlide = this._goToSlide.bind(this);
+    this._startAutoplay = this._startAutoplay.bind(this);
+    this._stopAutoplay = this._stopAutoplay.bind(this);
     this.state = {
       lastSlideIndex: null,
       slideReference: [],
       fullscreen: window.innerHeight === screen.height,
       mobile: window.innerWidth < 1000,
+      autoplaying: props.autoplay,
     };
   }
 
@@ -83,6 +91,9 @@ export default class Manager extends Component {
       lastSlideIndex: slideIndex,
     });
     this._attachEvents();
+    if (this.props.autoplay) {
+      this._startAutoplay();
+    }
   }
   componentDidUpdate() {
     if (
@@ -105,7 +116,18 @@ export default class Manager extends Component {
     window.removeEventListener('keydown', this._handleKeyPress);
     window.removeEventListener('resize', this._handleScreenChange);
   }
-  _handleEvent(e) {
+  _startAutoplay() {
+    clearInterval(this.autoplayInterval);
+    this.setState({ autoplaying: true });
+    this.autoplayInterval = setInterval(() => {
+      this._nextSlide();
+    }, this.props.autoplayDuration);
+  }
+  _stopAutoplay() {
+    this.setState({ autoplaying: false });
+    clearInterval(this.autoplayInterval);
+  }
+  _handleEvent(e) { // eslint-disable-line complexity
     const event = window.event ? window.event : e;
 
     if (
@@ -114,12 +136,14 @@ export default class Manager extends Component {
       (event.keyCode === 32 && event.shiftKey)
     ) {
       this._prevSlide();
+      this._stopAutoplay();
     } else if (
       event.keyCode === 39 ||
       event.keyCode === 34 ||
       (event.keyCode === 32 && !event.shiftKey)
     ) {
       this._nextSlide();
+      this._stopAutoplay();
     } else if (
       event.altKey &&
       event.keyCode === 79 &&
@@ -144,6 +168,16 @@ export default class Manager extends Component {
     ) {
       // t
       this._toggleTimerMode();
+    } else if (
+      event.altKey &&
+      event.keyCode === 65 &&
+      !event.ctrlKey &&
+      !event.metaKey
+    ) {
+      // a
+      if (this.props.autoplay) {
+        this._startAutoplay();
+      }
     }
   }
   _handleKeyPress(e) {
@@ -248,7 +282,13 @@ export default class Manager extends Component {
       this._checkFragments(this.props.route.slide, true) ||
       this.props.route.params.indexOf('overview') !== -1
     ) {
-      if (slideIndex < slideReference.length - 1) {
+      if (slideIndex === slideReference.length - 1) {
+        // On last slide, loop to first slide
+        if (this.props.autoplay && this.state.autoplaying) {
+          const slideData = '{ "slide": "0", "forward": "false" }';
+          this._goToSlide({ key: 'spectacle-slide', newValue: slideData });
+        }
+      } else if (slideIndex < slideReference.length - 1) {
         this.context.history.replace(
           `/${this._getHash(slideIndex + 1) + this._getSuffix()}`
         );
@@ -594,6 +634,14 @@ export default class Manager extends Component {
 
         {this.props.route.params.indexOf('export') === -1 ? <Fullscreen /> : ''}
 
+        {this.props.autoplay
+          ? <AutoplayControls
+              autoplaying={this.state.autoplaying}
+              onPlay={this._startAutoplay}
+              onPause={this._stopAutoplay}
+            />
+          : ''}
+
         {this.props.globalStyles &&
           <Style rules={Object.assign(this.context.styles.global, globals)} />}
       </div>
diff --git a/src/themes/default/screen.js b/src/themes/default/screen.js
index 8f56678..a57d6cb 100644
--- a/src/themes/default/screen.js
+++ b/src/themes/default/screen.js
@@ -49,6 +49,34 @@ const screen = (colorArgs = defaultColors, fontArgs = defaultFonts) => {
     fullscreen: {
       fill: colors.tertiary,
     },
+    autoplay: {
+      pause: {
+        position: 'absolute',
+        top: 20,
+        right: '50%',
+        left: '50%',
+        zIndex: 9999,
+        background: 'none',
+        border: 'none',
+        outline: 0,
+      },
+      pauseIcon: {
+        fill: colors.tertiary,
+      },
+      play: {
+        position: 'absolute',
+        top: 20,
+        right: '50%',
+        left: '50%',
+        zIndex: 9999,
+        background: 'none',
+        border: 'none',
+        outline: 0,
+      },
+      playIcon: {
+        fill: colors.tertiary,
+      },
+    },
     controls: {
       prev: {
         position: 'absolute',


Test patch:
diff --git a/src/components/__snapshots__/manager.test.js.snap b/src/components/__snapshots__/manager.test.js.snap
index 4bd6be3..3a2c980 100644
--- a/src/components/__snapshots__/manager.test.js.snap
+++ b/src/components/__snapshots__/manager.test.js.snap
@@ -11,6 +11,8 @@ exports[`<Manager /> should render correctly. 1`] = `
   transitionDuration={500}
 >
   <Manager
+    autoplay={false}
+    autoplayDuration={7000}
     controls={true}
     dispatch={[Function]}
     globalStyles={true}
@@ -259,6 +261,8 @@ exports[`<Manager /> should render correctly. 1`] = `
 exports[`<Manager /> should render the export configuration when specified. 1`] = `
 <Connect(Manager)>
   <Manager
+    autoplay={false}
+    autoplayDuration={7000}
     controls={true}
     dispatch={[Function]}
     globalStyles={true}
@@ -384,6 +388,8 @@ exports[`<Manager /> should render the export configuration when specified. 1`]
 exports[`<Manager /> should render the overview configuration when specified. 1`] = `
 <Connect(Manager)>
   <Manager
+    autoplay={false}
+    autoplayDuration={7000}
     controls={true}
     dispatch={[Function]}
     globalStyles={true}
@@ -559,6 +565,8 @@ exports[`<Manager /> should render the overview configuration when specified. 1`
 exports[`<Manager /> should render with slideset slides 1`] = `
 <Connect(Manager)>
   <Manager
+    autoplay={false}
+    autoplayDuration={7000}
     controls={true}
     dispatch={[Function]}
     globalStyles={true}