Repository: FormidableLabs/spectacle (ts)

Issue:
Support for autoplay 
Something like:

```
<Slide slideDuration={1000}>
  ...
</Slide>
```


Pull request description:
Add autoplay support
Resolves #42 

- Click pause/play buttons to toggle autoplay
- Press keys to advance slide or keys to go back a slide will stop autoplaying 
- Press `alt/option + a` to continue autoplaying 
- Set default autoplay duration to 7 seconds
- Automatically loop back to the beginning 

Thinking another PR could tackle a slide controlling its autoplay duration. Open to suggestions! 



Patch:
diff --git a/README.md b/README.md
index fd829b5..ebbbb54 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -326,6 +327,8 @@ The Deck tag is the root level tag for your presentation. It supports the follow
 |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)
diff --git a/example/src/index.js b/example/src/index.js
index fc5bf1a..95b40b2 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
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}