Skip to content

Commit f5e697b

Browse files
author
Carlos Paelinck
authored
Add GoToSlide button (#394)
* Add GoToSlide button * Removed errant console * Updated docs for GoToAction
1 parent 1cea368 commit f5e697b

File tree

8 files changed

+208
-8
lines changed

8 files changed

+208
-8
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ ReactJS based Presentation Library
4040
- [CodePane (Base)](#codepane-base)
4141
- [Code (Base)](#code-base)
4242
- [ComponentPlayground](#component-playground)
43+
- [GoToAction (Base)](#go-to-action)
4344
- [Heading (Base)](#heading-base)
4445
- [Image (Base)](#image-base)
4546
- [Link (Base)](#link-base)
@@ -523,6 +524,34 @@ class View extends React.Component {
523524
render(<View />);
524525
```
525526

527+
<a name="go-to-action"></a>
528+
#### Go To Action (Base)
529+
530+
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.
531+
532+
|Name|PropType|Description|
533+
|---|---|---|
534+
|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.
535+
|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.
536+
537+
##### Simple Button Configuration Example
538+
```jsx
539+
<GoToAction slide={3}>Jump to 3</GoToAction>
540+
```
541+
542+
##### Custom Component Configuration Example
543+
```jsx
544+
<GoToAction
545+
render={goToSlide => (
546+
<CustomComponent onClick={() => goToSlide("wait-wut")}>
547+
WAIT WUT!?
548+
</CustomComponent>
549+
)}
550+
/>
551+
```
552+
553+
554+
526555
<a name="heading-base"></a>
527556
#### Heading (Base)
528557

example/src/index.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import {
44
Appear, BlockQuote, Cite, CodePane, ComponentPlayground, Deck, Fill,
55
Heading, Image, Layout, Link, ListItem, List, Markdown, MarkdownSlides, Quote, Slide, SlideSet,
6-
TableBody, TableHeader, TableHeaderItem, TableItem, TableRow, Table, Text, S
6+
TableBody, TableHeader, TableHeaderItem, TableItem, TableRow, Table, Text, GoToAction
77
} from '../../src';
88

99
import preloader from '../../src/utils/preloader';
@@ -116,6 +116,38 @@ export default class Presentation extends React.Component {
116116
</Heading>
117117
</Appear>
118118
</Slide>
119+
<Slide>
120+
<Heading size={2} textColor="secondary" margin="0.25em">
121+
Mix it up!
122+
</Heading>
123+
<Heading size={6} textColor="tertiary">
124+
You can even jump to different slides with a standard button or custom component!
125+
</Heading>
126+
<GoToAction
127+
margin="1em"
128+
slide={8}
129+
>
130+
Jump to Slide 8
131+
</GoToAction>
132+
<GoToAction
133+
render={goToSlide => (
134+
<select
135+
defaultValue=""
136+
style={{
137+
background: '#000',
138+
color: '#fff',
139+
fontFamily: theme.print.fonts.primary,
140+
fontSize: '1.1em'
141+
}}
142+
onChange={({ target }) => goToSlide(target.value)}
143+
>
144+
<option value="" disabled>Custom Slide Picker</option>
145+
<option value="wait-what">Wait What!? Slide</option>
146+
<option value={2}>Slide 2</option>
147+
</select>
148+
)}
149+
/>
150+
</Slide>
119151
<Slide transition={['slide']} bgDarken={0.75} getAppearStep={this.updateSteps}>
120152
<Appear>
121153
<Heading size={1} caps textColor="tertiary">
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`<GoToAction /> should just render a div when no props are provided 1`] = `
4+
<GoToAction>
5+
<div />
6+
</GoToAction>
7+
`;

src/components/go-to-action.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { getStyles } from '../utils/base';
4+
import styled from 'react-emotion';
5+
import isFunction from 'lodash/isFunction';
6+
7+
const GoToActionButton = styled.button(({ styles }) => [
8+
styles.context,
9+
styles.base,
10+
styles.user
11+
]);
12+
13+
class GoToAction extends React.Component {
14+
render() {
15+
const {
16+
props: { render, children, style, slide },
17+
context: { goToSlide }
18+
} = this;
19+
if (render && isFunction(render)) {
20+
return render(goToSlide);
21+
} else if (slide) {
22+
return (
23+
<GoToActionButton
24+
onClick={() => goToSlide(slide)}
25+
styles={{
26+
context: this.context.styles.components.goToAction,
27+
base: getStyles.call(this),
28+
user: style
29+
}}
30+
>
31+
{children}
32+
</GoToActionButton>
33+
);
34+
}
35+
// eslint-disable-next-line no-console
36+
console.warn('<GoToAction /> must have a render or slide prop.');
37+
return <div />;
38+
}
39+
}
40+
41+
GoToAction.propTypes = {
42+
children: PropTypes.node,
43+
render: PropTypes.func,
44+
slide: PropTypes.oneOfType([
45+
PropTypes.number, PropTypes.string
46+
]),
47+
style: PropTypes.object,
48+
};
49+
50+
GoToAction.contextTypes = {
51+
styles: PropTypes.object,
52+
goToSlide: PropTypes.func,
53+
};
54+
55+
export default GoToAction;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import { mount } from 'enzyme';
3+
import GoToAction from './go-to-action';
4+
5+
describe('<GoToAction />', () => {
6+
test('should call the context function with the slide prop when it has a child', () => {
7+
const stub = jest.fn();
8+
const context = {
9+
styles: { components: { goToAction: {} } },
10+
goToSlide: stub
11+
};
12+
const wrapper = mount(
13+
<GoToAction slide={2}>Slide 2</GoToAction>, { context }
14+
);
15+
wrapper.simulate('click');
16+
expect(stub).toHaveBeenCalledTimes(1);
17+
expect(stub).toHaveBeenCalledWith(2);
18+
});
19+
20+
test('should call the context function when providing a custom component', () => {
21+
const stub = jest.fn();
22+
const context = {
23+
styles: { components: { goToAction: {} } },
24+
goToSlide: stub
25+
};
26+
const wrapper = mount(
27+
<GoToAction
28+
render={goToSlide => (
29+
<button id="inner-btn" onClick={() => goToSlide('wait-what')}>
30+
WAIT WUT
31+
</button>
32+
)}
33+
/>, { context }
34+
);
35+
wrapper.find('button#inner-btn').simulate('click');
36+
expect(stub).toHaveBeenCalledTimes(1);
37+
expect(stub).toHaveBeenCalledWith('wait-what');
38+
});
39+
40+
test('should just render a div when no props are provided', () => {
41+
const context = {
42+
styles: { components: { goToAction: {} } },
43+
goToSlide: () => {}
44+
};
45+
const wrapper = mount(
46+
<GoToAction />, { context }
47+
);
48+
expect(wrapper).toMatchSnapshot();
49+
});
50+
});

src/components/manager.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export class Manager extends Component {
9696
static childContextTypes = {
9797
contentWidth: PropTypes.number,
9898
contentHeight: PropTypes.number,
99+
goToSlide: PropTypes.func
99100
};
100101

101102
constructor(props) {
@@ -119,6 +120,7 @@ export class Manager extends Component {
119120
return {
120121
contentWidth: this.props.contentWidth,
121122
contentHeight: this.props.contentHeight,
123+
goToSlide: slide => this._goToSlide({ slide })
122124
};
123125
}
124126

@@ -273,15 +275,26 @@ export class Manager extends Component {
273275
}
274276
}
275277
_goToSlide(e) {
278+
let data = null;
279+
let canNavigate = true;
276280
if (e.key === 'spectacle-slide') {
277-
const data = JSON.parse(e.newValue);
278-
const slideIndex = this._getSlideIndex();
279-
this.setState({
280-
lastSlideIndex: slideIndex || 0,
281-
});
282-
if (this._checkFragments(this.props.route.slide, data.forward)) {
283-
this.context.history.replace(`/${data.slide}${this._getSuffix()}`);
281+
canNavigate = this._checkFragments(this.props.route.slide, data.forward);
282+
data = JSON.parse(e.newValue);
283+
} else if (e.slide) {
284+
data = e;
285+
} else {
286+
return;
287+
}
288+
const slideIndex = this._getSlideIndex();
289+
this.setState({
290+
lastSlideIndex: slideIndex || 0,
291+
});
292+
if (canNavigate) {
293+
let slide = data.slide;
294+
if (typeof slide === 'number') {
295+
slide -= 1;
284296
}
297+
this.context.history.replace(`/${slide}${this._getSuffix()}`);
285298
}
286299
}
287300
_prevSlide() {

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Fill } from './components/fill';
99
import { Fit } from './components/fit';
1010
import Heading from './components/heading';
1111
import Image from './components/image';
12+
import GoToAction from './components/go-to-action';
1213
import Layout from './components/layout';
1314
import Link from './components/link';
1415
import ListItem from './components/list-item';
@@ -48,6 +49,7 @@ export {
4849
Fit,
4950
Heading,
5051
Image,
52+
GoToAction,
5153
Layout,
5254
Link,
5355
ListItem,

src/themes/default/screen.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,18 @@ const screen = (colorArgs = defaultColors, fontArgs = defaultFonts) => {
230230
padding: '0 10px',
231231
borderRadius: 3,
232232
},
233+
goToAction: {
234+
borderRadius: '6px',
235+
fontFamily: fonts.primary,
236+
padding: '0.25em 1em',
237+
border: 'none',
238+
background: '#000',
239+
color: '#fff',
240+
'&:hover': {
241+
background: colors.tertiary,
242+
color: '#000'
243+
}
244+
},
233245
heading: {
234246
h1: {
235247
color: colors.tertiary,

0 commit comments

Comments
 (0)