diff --git a/README.md b/README.md
index d0a897f3e..ca4e85359 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ ReactJS based Presentation Library
- [CodePane (Base)](#codepane-base)
- [Code (Base)](#code-base)
- [ComponentPlayground](#component-playground)
+ - [GoToAction (Base)](#go-to-action)
- [Heading (Base)](#heading-base)
- [Image (Base)](#image-base)
- [Link (Base)](#link-base)
@@ -523,6 +524,34 @@ class View extends React.Component {
render();
```
+
+#### Go To Action (Base)
+
+The GoToAction tag lets you jump to another slide in your deck. The GoToAction can be used a simple button that supports `Base` styling or accept a render prop with a callback to support custom components.
+
+|Name|PropType|Description|
+|---|---|---|
+|slide|PropTypes.string or PropTypes.number|The string identifier or number of the side the button should jump to. Slide numbers start at `1`. This is only used in the simple button configuration.
+|render|PropTypes.func|A function with a `goToSlide` param that should return a React element to render. This is only used in the custom component configuration.
+
+##### Simple Button Configuration Example
+```jsx
+Jump to 3
+```
+
+##### Custom Component Configuration Example
+```jsx
+ (
+ goToSlide("wait-wut")}>
+ WAIT WUT!?
+
+ )}
+/>
+```
+
+
+
#### Heading (Base)
diff --git a/example/src/index.js b/example/src/index.js
index d2a5ecf39..104b0c705 100644
--- a/example/src/index.js
+++ b/example/src/index.js
@@ -3,7 +3,7 @@ import React from 'react';
import {
Appear, BlockQuote, Cite, CodePane, ComponentPlayground, Deck, Fill,
Heading, Image, Layout, Link, ListItem, List, Markdown, MarkdownSlides, Quote, Slide, SlideSet,
- TableBody, TableHeader, TableHeaderItem, TableItem, TableRow, Table, Text, S
+ TableBody, TableHeader, TableHeaderItem, TableItem, TableRow, Table, Text, GoToAction
} from '../../src';
import preloader from '../../src/utils/preloader';
@@ -116,6 +116,38 @@ export default class Presentation extends React.Component {
+
+
+ Mix it up!
+
+
+ You can even jump to different slides with a standard button or custom component!
+
+
+ Jump to Slide 8
+
+ (
+
+ )}
+ />
+
diff --git a/src/components/__snapshots__/go-to-action.test.js.snap b/src/components/__snapshots__/go-to-action.test.js.snap
new file mode 100644
index 000000000..a45efb734
--- /dev/null
+++ b/src/components/__snapshots__/go-to-action.test.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` should just render a div when no props are provided 1`] = `
+
+
+
+`;
diff --git a/src/components/go-to-action.js b/src/components/go-to-action.js
new file mode 100644
index 000000000..58dc57e49
--- /dev/null
+++ b/src/components/go-to-action.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { getStyles } from '../utils/base';
+import styled from 'react-emotion';
+import isFunction from 'lodash/isFunction';
+
+const GoToActionButton = styled.button(({ styles }) => [
+ styles.context,
+ styles.base,
+ styles.user
+]);
+
+class GoToAction extends React.Component {
+ render() {
+ const {
+ props: { render, children, style, slide },
+ context: { goToSlide }
+ } = this;
+ if (render && isFunction(render)) {
+ return render(goToSlide);
+ } else if (slide) {
+ return (
+ goToSlide(slide)}
+ styles={{
+ context: this.context.styles.components.goToAction,
+ base: getStyles.call(this),
+ user: style
+ }}
+ >
+ {children}
+
+ );
+ }
+ // eslint-disable-next-line no-console
+ console.warn(' must have a render or slide prop.');
+ return ;
+ }
+}
+
+GoToAction.propTypes = {
+ children: PropTypes.node,
+ render: PropTypes.func,
+ slide: PropTypes.oneOfType([
+ PropTypes.number, PropTypes.string
+ ]),
+ style: PropTypes.object,
+};
+
+GoToAction.contextTypes = {
+ styles: PropTypes.object,
+ goToSlide: PropTypes.func,
+};
+
+export default GoToAction;
diff --git a/src/components/go-to-action.test.js b/src/components/go-to-action.test.js
new file mode 100644
index 000000000..1ffb1888d
--- /dev/null
+++ b/src/components/go-to-action.test.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import GoToAction from './go-to-action';
+
+describe('', () => {
+ test('should call the context function with the slide prop when it has a child', () => {
+ const stub = jest.fn();
+ const context = {
+ styles: { components: { goToAction: {} } },
+ goToSlide: stub
+ };
+ const wrapper = mount(
+ Slide 2, { context }
+ );
+ wrapper.simulate('click');
+ expect(stub).toHaveBeenCalledTimes(1);
+ expect(stub).toHaveBeenCalledWith(2);
+ });
+
+ test('should call the context function when providing a custom component', () => {
+ const stub = jest.fn();
+ const context = {
+ styles: { components: { goToAction: {} } },
+ goToSlide: stub
+ };
+ const wrapper = mount(
+ (
+
+ )}
+ />, { context }
+ );
+ wrapper.find('button#inner-btn').simulate('click');
+ expect(stub).toHaveBeenCalledTimes(1);
+ expect(stub).toHaveBeenCalledWith('wait-what');
+ });
+
+ test('should just render a div when no props are provided', () => {
+ const context = {
+ styles: { components: { goToAction: {} } },
+ goToSlide: () => {}
+ };
+ const wrapper = mount(
+ , { context }
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/src/components/manager.js b/src/components/manager.js
index f1dd59b9b..24d4c41b1 100644
--- a/src/components/manager.js
+++ b/src/components/manager.js
@@ -96,6 +96,7 @@ export class Manager extends Component {
static childContextTypes = {
contentWidth: PropTypes.number,
contentHeight: PropTypes.number,
+ goToSlide: PropTypes.func
};
constructor(props) {
@@ -119,6 +120,7 @@ export class Manager extends Component {
return {
contentWidth: this.props.contentWidth,
contentHeight: this.props.contentHeight,
+ goToSlide: slide => this._goToSlide({ slide })
};
}
@@ -273,15 +275,26 @@ export class Manager extends Component {
}
}
_goToSlide(e) {
+ let data = null;
+ let canNavigate = true;
if (e.key === 'spectacle-slide') {
- const data = JSON.parse(e.newValue);
- const slideIndex = this._getSlideIndex();
- this.setState({
- lastSlideIndex: slideIndex || 0,
- });
- if (this._checkFragments(this.props.route.slide, data.forward)) {
- this.context.history.replace(`/${data.slide}${this._getSuffix()}`);
+ canNavigate = this._checkFragments(this.props.route.slide, data.forward);
+ data = JSON.parse(e.newValue);
+ } else if (e.slide) {
+ data = e;
+ } else {
+ return;
+ }
+ const slideIndex = this._getSlideIndex();
+ this.setState({
+ lastSlideIndex: slideIndex || 0,
+ });
+ if (canNavigate) {
+ let slide = data.slide;
+ if (typeof slide === 'number') {
+ slide -= 1;
}
+ this.context.history.replace(`/${slide}${this._getSuffix()}`);
}
}
_prevSlide() {
diff --git a/src/index.js b/src/index.js
index 42f2c204f..aebf026a7 100644
--- a/src/index.js
+++ b/src/index.js
@@ -9,6 +9,7 @@ import { Fill } from './components/fill';
import { Fit } from './components/fit';
import Heading from './components/heading';
import Image from './components/image';
+import GoToAction from './components/go-to-action';
import Layout from './components/layout';
import Link from './components/link';
import ListItem from './components/list-item';
@@ -48,6 +49,7 @@ export {
Fit,
Heading,
Image,
+ GoToAction,
Layout,
Link,
ListItem,
diff --git a/src/themes/default/screen.js b/src/themes/default/screen.js
index 93c2a7c68..d61899cb4 100644
--- a/src/themes/default/screen.js
+++ b/src/themes/default/screen.js
@@ -230,6 +230,18 @@ const screen = (colorArgs = defaultColors, fontArgs = defaultFonts) => {
padding: '0 10px',
borderRadius: 3,
},
+ goToAction: {
+ borderRadius: '6px',
+ fontFamily: fonts.primary,
+ padding: '0.25em 1em',
+ border: 'none',
+ background: '#000',
+ color: '#fff',
+ '&:hover': {
+ background: colors.tertiary,
+ color: '#000'
+ }
+ },
heading: {
h1: {
color: colors.tertiary,